From b872228ec6687a0a0182e595be374b994fbe040e Mon Sep 17 00:00:00 2001 From: fisehara Date: Fri, 31 Mar 2023 15:46:36 +0200 Subject: [PATCH 1/4] Return $metadata resource as odata + openapi spec Returning odata and openapi specs in json format. Specs are scoped to the request permissions. Different users (roles) will receive different metadata endpoints and resources. Change-type: minor Signed-off-by: fisehara --- package.json | 1 + .../odata-metadata-generator.ts | 473 +++++++++++++----- .../open-api-sepcification-generator.ts | 77 +++ src/sbvr-api/permissions.ts | 4 +- src/sbvr-api/sbvr-utils.ts | 34 +- src/sbvr-api/uri-parser.ts | 2 +- test/04-metadata.test.ts | 47 ++ .../04-metadata/config-full-access.ts | 18 + .../04-metadata/config-restricted-access.ts | 25 + test/fixtures/04-metadata/example.sbvr | 33 ++ typings/odata-openapi.d.ts | 6 + 11 files changed, 587 insertions(+), 133 deletions(-) create mode 100644 src/odata-metadata/open-api-sepcification-generator.ts create mode 100644 test/04-metadata.test.ts create mode 100644 test/fixtures/04-metadata/config-full-access.ts create mode 100644 test/fixtures/04-metadata/config-restricted-access.ts create mode 100644 test/fixtures/04-metadata/example.sbvr create mode 100644 typings/odata-openapi.d.ts diff --git a/package.json b/package.json index 197d6488a..9b0bab5fb 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "lodash": "^4.17.21", "memoizee": "^0.4.15", "pinejs-client-core": "^6.12.3", + "odata-openapi": "^0.19.1", "randomstring": "^1.2.3", "typed-error": "^3.2.1" }, 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..551344f85 100644 --- a/src/sbvr-api/sbvr-utils.ts +++ b/src/sbvr-api/sbvr-utils.ts @@ -36,7 +36,7 @@ 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'; // tslint:disable-next-line:no-var-requires const devModel = require('./dev.sbvr'); @@ -95,6 +95,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 +1711,35 @@ 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 === '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 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 { // TODO: request.resourceName can be '$serviceroot' or a resource and we should return an odata xml document based on that @@ -1724,6 +1750,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/04-metadata.test.ts b/test/04-metadata.test.ts new file mode 100644 index 000000000..d365673ed --- /dev/null +++ b/test/04-metadata.test.ts @@ -0,0 +1,47 @@ +import * as fs from 'fs'; +import { expect } from 'chai'; +import * as supertest from 'supertest'; +import { testInit, testDeInit, testLocalServer } from './lib/test-init'; + +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('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'); + + fs.writeFileSync('./openapiSpec.json', JSON.stringify(res.body, null, 2)); + + 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; +} From ecbf059fcb9fe87964356d0ee6b10ea5223ea26b Mon Sep 17 00:00:00 2001 From: fisehara Date: Fri, 31 Mar 2023 22:09:28 +0200 Subject: [PATCH 2/4] Experimenting with Fern API Generators Signed-off-by: fisehara --- fern/api/definition/api.yml | 1 + fern/api/definition/example.yaml | 295 ++++++++ fern/api/generators.yml | 43 ++ fern/api/open-api/openapi.yml | 694 +++++++++++++++++++ fern/fern.config.json | 4 + package.json | 277 ++++---- src/odata-metadata/fern-metadatagenerator.ts | 343 +++++++++ src/sbvr-api/sbvr-utils.ts | 17 +- test/01-constrain.test.ts | 29 +- test/04-metadata.test.ts | 24 +- 10 files changed, 1583 insertions(+), 144 deletions(-) create mode 100644 fern/api/definition/api.yml create mode 100644 fern/api/definition/example.yaml create mode 100644 fern/api/generators.yml create mode 100644 fern/api/open-api/openapi.yml create mode 100644 fern/fern.config.json create mode 100644 src/odata-metadata/fern-metadatagenerator.ts 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..66ef0e674 --- /dev/null +++ b/fern/api/definition/example.yaml @@ -0,0 +1,295 @@ +service: + auth: false + base-path: /example + endpoints: + ReadAllApplication: + path: /application + method: GET + response: map> + request: + name: ReadallApplication + query-parameters: + $filter: optional + $select: optional + $expand: optional + $top: optional + $count: optional + ReadApplication: + path: /application(id) + method: GET + response: Application + request: + name: ReadApplicationById + CreateApplication: + path: /application(id) + method: POST + response: Application + request: + name: CreateApplicationById + UpdateAllApplication: + path: /application + method: PATCH + request: + name: UpdateallApplication + query-parameters: + $filter: optional + $select: optional + $expand: optional + $top: optional + $count: optional + UpdateApplication: + path: /application(id) + method: PATCH + request: + name: UpdateApplicationById + DeleteAllApplication: + path: /application + method: DELETE + request: + name: DeleteallApplication + query-parameters: + $filter: optional + $select: optional + $expand: optional + $top: optional + $count: optional + DeleteApplication: + path: /application(id) + method: DELETE + request: + name: DeleteApplicationById + ReadAllDevice: + path: /device + method: GET + response: map> + request: + name: ReadallDevice + query-parameters: + $filter: optional + $select: optional + $expand: optional + $top: optional + $count: optional + ReadDevice: + path: /device(id) + method: GET + response: Device + request: + name: ReadDeviceById + CreateDevice: + path: /device(id) + method: POST + response: Device + request: + name: CreateDeviceById + UpdateAllDevice: + path: /device + method: PATCH + request: + name: UpdateallDevice + query-parameters: + $filter: optional + $select: optional + $expand: optional + $top: optional + $count: optional + UpdateDevice: + path: /device(id) + method: PATCH + request: + name: UpdateDeviceById + DeleteAllDevice: + path: /device + method: DELETE + request: + name: DeleteallDevice + query-parameters: + $filter: optional + $select: optional + $expand: optional + $top: optional + $count: optional + DeleteDevice: + path: /device(id) + method: DELETE + request: + name: DeleteDeviceById + ReadAllGateway: + path: /gateway + method: GET + response: map> + request: + name: ReadallGateway + query-parameters: + $filter: optional + $select: optional + $expand: optional + $top: optional + $count: optional + ReadGateway: + path: /gateway(id) + method: GET + response: Gateway + request: + name: ReadGatewayById + CreateGateway: + path: /gateway(id) + method: POST + response: Gateway + request: + name: CreateGatewayById + UpdateAllGateway: + path: /gateway + method: PATCH + request: + name: UpdateallGateway + query-parameters: + $filter: optional + $select: optional + $expand: optional + $top: optional + $count: optional + UpdateGateway: + path: /gateway(id) + method: PATCH + request: + name: UpdateGatewayById + DeleteAllGateway: + path: /gateway + method: DELETE + request: + name: DeleteallGateway + query-parameters: + $filter: optional + $select: optional + $expand: optional + $top: optional + $count: optional + DeleteGateway: + path: /gateway(id) + method: DELETE + request: + name: DeleteGatewayById + ReadAllGateway__connects__device: + path: /gateway__connects__device + method: GET + response: map> + request: + name: ReadallGateway__connects__device + query-parameters: + $filter: optional + $select: optional + $expand: optional + $top: optional + $count: optional + ReadGateway__connects__device: + path: /gateway__connects__device(id) + method: GET + response: Gateway__connects__device + request: + name: ReadGateway__connects__deviceById + CreateGateway__connects__device: + path: /gateway__connects__device(id) + method: POST + response: Gateway__connects__device + request: + name: CreateGateway__connects__deviceById + 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 + UpdateGateway__connects__device: + path: /gateway__connects__device(id) + method: PATCH + request: + name: UpdateGateway__connects__deviceById + 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 + DeleteGateway__connects__device: + path: /gateway__connects__device(id) + method: DELETE + request: + name: DeleteGateway__connects__deviceById +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: + id: 66373 + name: ttpffnhlhdcvtdofbdel + note: ggewjgbmvhrxrzlquold + 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: + id: 50431 + name: ifmxoghvncqheqxwwenr + type: rljywhrcghwlhcgihtdz + 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: + id: 83118 + name: tvsdvgvfljyfazvjfwbb + 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: + id: 20562 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..1d17f1e1d --- /dev/null +++ b/fern/api/open-api/openapi.yml @@ -0,0 +1,694 @@ +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 + - 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: + type: object + additionalProperties: + type: array + items: + $ref: '#/components/schemas/Application' + 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 + - 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 + - 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' + post: + operationId: example_CreateApplication + tags: + - Example + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Application' + 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 + - 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: + type: object + additionalProperties: + type: array + items: + $ref: '#/components/schemas/Device' + 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 + - 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 + - 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' + post: + operationId: example_CreateDevice + tags: + - Example + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Device' + 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 + - 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: + type: object + additionalProperties: + type: array + items: + $ref: '#/components/schemas/Gateway' + 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 + - 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 + - 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' + post: + operationId: example_CreateGateway + tags: + - Example + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Gateway' + 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 + - 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: + type: object + additionalProperties: + type: array + items: + $ref: '#/components/schemas/Gateway__connects__device' + 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 + - 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 + - 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' + post: + operationId: example_CreateGateway__connects__device + tags: + - Example + parameters: [] + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Gateway__connects__device' + 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: 66373 + name: + type: string + example: ttpffnhlhdcvtdofbdel + note: + type: string + example: ggewjgbmvhrxrzlquold + required: + - id + - name + - note + 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: 50431 + name: + type: string + example: ifmxoghvncqheqxwwenr + type: + type: string + belongs_to__application: + $ref: '#/components/schemas/Application' + required: + - id + - name + 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: 83118 + name: + type: string + required: + - id + 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: 20562 + gateway: + $ref: '#/components/schemas/Gateway' + connects__device: + $ref: '#/components/schemas/Device' + required: + - id + securitySchemes: {} diff --git a/fern/fern.config.json b/fern/fern.config.json new file mode 100644 index 000000000..3632f9023 --- /dev/null +++ b/fern/fern.config.json @@ -0,0 +1,4 @@ +{ + "organization": "fern", + "version": "0.6.4" + } \ No newline at end of file diff --git a/package.json b/package.json index 9b0bab5fb..aa25e1327 100644 --- a/package.json +++ b/package.json @@ -1,138 +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", - "odata-openapi": "^0.19.1", - "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.4", + "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..e8db3ad1e --- /dev/null +++ b/src/odata-metadata/fern-metadatagenerator.ts @@ -0,0 +1,343 @@ +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 exampleForFields: 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)}`, + }; + } + exampleForFields[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; + + // exampleForFields[fieldName] = exampleFaker(fieldName, 'id'); + }); + + const capitalizedResourceName = capitalize(resourceName); + + uniqueTable.examples ??= [{ value: exampleForFields }]; + + fernRootTypes[capitalizedResourceName] = uniqueTable; + + 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, + create: capitalizedResourceName, + 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] + ? `map>` + : undefined, + request: { + name: capitalize(resKey) + 'all' + capitalizedResourceName, + 'query-parameters': ODataQueryParameters, + }, + // examples: [ + // { + // 'query-parameters': { + // $select: selectableFields.join(','), + // }, + // response: compileResponse[resKey] + // ? { + // body: { + // d: [exampleForFields, exampleForFields], + // }, + // } + // : 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: exampleForFields, + // } + // : undefined, + // }, + // ], + }; + } + } + }); + + const fernRootApi = { + service: { + auth: false, + 'base-path': `/${vocabulary}`, + endpoints: fernRootEndpoints, + }, + types: fernRootTypes, + // errors: fernRootErrors, + }; + + return fernRootApi; +}; + +generateFernMetadata.version = version; diff --git a/src/sbvr-api/sbvr-utils.ts b/src/sbvr-api/sbvr-utils.ts index 551344f85..7e241f7e7 100644 --- a/src/sbvr-api/sbvr-utils.ts +++ b/src/sbvr-api/sbvr-utils.ts @@ -37,6 +37,7 @@ import { ExtendedSBVRParser } from '../extended-sbvr-parser/extended-sbvr-parser import * as asyncMigrator from '../migrator/async'; import * as syncMigrator from '../migrator/sync'; 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'); @@ -1722,7 +1723,7 @@ const respondGet = async ( body: spec, headers: { 'content-type': 'application/json' }, }; - } else if (request.resourceName === 'openapi.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); @@ -1741,6 +1742,20 @@ const respondGet = async ( 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 return { 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 index d365673ed..54cb01ffc 100644 --- a/test/04-metadata.test.ts +++ b/test/04-metadata.test.ts @@ -3,6 +3,8 @@ 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'; @@ -29,19 +31,29 @@ describe('04 metadata', function () { expect(res.body).to.be.an('object'); }); - it('OpenAPI spec should contain all paths and actions on resources', async () => { + 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'); - fs.writeFileSync('./openapiSpec.json', JSON.stringify(res.body, null, 2)); + 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']); - } + // 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']); + // } }); }); }); From a0c25ec63194498e1f3eb5032c6d665407e35ebd Mon Sep 17 00:00:00 2001 From: fisehara Date: Fri, 31 Mar 2023 22:13:49 +0200 Subject: [PATCH 3/4] Examples for Types and Endpoints Signed-off-by: fisehara --- fern/api/definition/example.yaml | 142 +++++++++++++++++-- fern/api/open-api/openapi.yml | 126 +++++++++++++++- src/odata-metadata/fern-metadatagenerator.ts | 56 ++++---- 3 files changed, 281 insertions(+), 43 deletions(-) diff --git a/fern/api/definition/example.yaml b/fern/api/definition/example.yaml index 66ef0e674..d1975a28c 100644 --- a/fern/api/definition/example.yaml +++ b/fern/api/definition/example.yaml @@ -14,18 +14,42 @@ service: $expand: optional $top: optional $count: optional + examples: + - query-parameters: + $select: created_at,modified_at,id,name,note + response: + body: + d: + - id: 4667 + name: asazpqprbwfzcgqpwggq + note: eqydvcitnsirwajnugcd + - id: 4667 + name: asazpqprbwfzcgqpwggq + note: eqydvcitnsirwajnugcd ReadApplication: path: /application(id) method: GET response: Application request: name: ReadApplicationById + examples: + - response: + body: + id: 4667 + name: asazpqprbwfzcgqpwggq + note: eqydvcitnsirwajnugcd CreateApplication: path: /application(id) method: POST response: Application request: name: CreateApplicationById + examples: + - response: + body: + id: 4667 + name: asazpqprbwfzcgqpwggq + note: eqydvcitnsirwajnugcd UpdateAllApplication: path: /application method: PATCH @@ -37,11 +61,16 @@ service: $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 @@ -53,11 +82,16 @@ service: $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 @@ -70,18 +104,42 @@ service: $expand: optional $top: optional $count: optional + examples: + - query-parameters: + $select: created_at,modified_at,id,name,type,belongs_to__application + response: + body: + d: + - id: 82776 + name: kwyepepbhyjahnafvbmz + type: duxabktuxrqniguywjzn + - id: 82776 + name: kwyepepbhyjahnafvbmz + type: duxabktuxrqniguywjzn ReadDevice: path: /device(id) method: GET response: Device request: name: ReadDeviceById + examples: + - response: + body: + id: 82776 + name: kwyepepbhyjahnafvbmz + type: duxabktuxrqniguywjzn CreateDevice: path: /device(id) method: POST response: Device request: name: CreateDeviceById + examples: + - response: + body: + id: 82776 + name: kwyepepbhyjahnafvbmz + type: duxabktuxrqniguywjzn UpdateAllDevice: path: /device method: PATCH @@ -93,11 +151,16 @@ service: $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 @@ -109,11 +172,16 @@ service: $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 @@ -126,18 +194,38 @@ service: $expand: optional $top: optional $count: optional + examples: + - query-parameters: + $select: created_at,modified_at,id,name + response: + body: + d: + - id: 93907 + name: grwlnjidfweccerzvxgo + - id: 93907 + name: grwlnjidfweccerzvxgo ReadGateway: path: /gateway(id) method: GET response: Gateway request: name: ReadGatewayById + examples: + - response: + body: + id: 93907 + name: grwlnjidfweccerzvxgo CreateGateway: path: /gateway(id) method: POST response: Gateway request: name: CreateGatewayById + examples: + - response: + body: + id: 93907 + name: grwlnjidfweccerzvxgo UpdateAllGateway: path: /gateway method: PATCH @@ -149,11 +237,16 @@ service: $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 @@ -165,11 +258,16 @@ service: $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 @@ -182,18 +280,34 @@ service: $expand: optional $top: optional $count: optional + examples: + - query-parameters: + $select: created_at,modified_at,id,gateway,connects__device + response: + body: + d: + - id: 58529 + - id: 58529 ReadGateway__connects__device: path: /gateway__connects__device(id) method: GET response: Gateway__connects__device request: name: ReadGateway__connects__deviceById + examples: + - response: + body: + id: 58529 CreateGateway__connects__device: path: /gateway__connects__device(id) method: POST response: Gateway__connects__device request: name: CreateGateway__connects__deviceById + examples: + - response: + body: + id: 58529 UpdateAllGateway__connects__device: path: /gateway__connects__device method: PATCH @@ -205,11 +319,16 @@ service: $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 @@ -221,11 +340,16 @@ service: $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: @@ -242,9 +366,9 @@ types: type: string examples: - value: - id: 66373 - name: ttpffnhlhdcvtdofbdel - note: ggewjgbmvhrxrzlquold + id: 4667 + name: asazpqprbwfzcgqpwggq + note: eqydvcitnsirwajnugcd Device: properties: created_at: @@ -261,9 +385,9 @@ types: belongs_to__application: optional examples: - value: - id: 50431 - name: ifmxoghvncqheqxwwenr - type: rljywhrcghwlhcgihtdz + id: 82776 + name: kwyepepbhyjahnafvbmz + type: duxabktuxrqniguywjzn Gateway: properties: created_at: @@ -277,8 +401,8 @@ types: type: optional examples: - value: - id: 83118 - name: tvsdvgvfljyfazvjfwbb + id: 93907 + name: grwlnjidfweccerzvxgo Gateway__connects__device: properties: created_at: @@ -292,4 +416,4 @@ types: connects__device: optional examples: - value: - id: 20562 + id: 58529 diff --git a/fern/api/open-api/openapi.yml b/fern/api/open-api/openapi.yml index 1d17f1e1d..2d0651ec2 100644 --- a/fern/api/open-api/openapi.yml +++ b/fern/api/open-api/openapi.yml @@ -19,6 +19,9 @@ paths: required: false schema: type: string + examples: + Example1: + value: created_at,modified_at,id,name,note - name: $expand in: query required: false @@ -45,6 +48,16 @@ paths: type: array items: $ref: '#/components/schemas/Application' + examples: + Example1: + value: + d: + - id: 4667 + name: asazpqprbwfzcgqpwggq + note: eqydvcitnsirwajnugcd + - id: 4667 + name: asazpqprbwfzcgqpwggq + note: eqydvcitnsirwajnugcd patch: operationId: example_UpdateAllApplication tags: @@ -60,6 +73,9 @@ paths: required: false schema: type: string + examples: + Example1: + value: created_at,modified_at,id,name,note - name: $expand in: query required: false @@ -93,6 +109,9 @@ paths: required: false schema: type: string + examples: + Example1: + value: created_at,modified_at,id,name,note - name: $expand in: query required: false @@ -124,6 +143,12 @@ paths: application/json: schema: $ref: '#/components/schemas/Application' + examples: + Example1: + value: + id: 4667 + name: asazpqprbwfzcgqpwggq + note: eqydvcitnsirwajnugcd post: operationId: example_CreateApplication tags: @@ -136,6 +161,12 @@ paths: application/json: schema: $ref: '#/components/schemas/Application' + examples: + Example1: + value: + id: 4667 + name: asazpqprbwfzcgqpwggq + note: eqydvcitnsirwajnugcd patch: operationId: example_UpdateApplication tags: @@ -168,6 +199,9 @@ paths: required: false schema: type: string + examples: + Example1: + value: created_at,modified_at,id,name,type,belongs_to__application - name: $expand in: query required: false @@ -194,6 +228,16 @@ paths: type: array items: $ref: '#/components/schemas/Device' + examples: + Example1: + value: + d: + - id: 82776 + name: kwyepepbhyjahnafvbmz + type: duxabktuxrqniguywjzn + - id: 82776 + name: kwyepepbhyjahnafvbmz + type: duxabktuxrqniguywjzn patch: operationId: example_UpdateAllDevice tags: @@ -209,6 +253,9 @@ paths: required: false schema: type: string + examples: + Example1: + value: created_at,modified_at,id,name,type,belongs_to__application - name: $expand in: query required: false @@ -242,6 +289,9 @@ paths: required: false schema: type: string + examples: + Example1: + value: created_at,modified_at,id,name,type,belongs_to__application - name: $expand in: query required: false @@ -273,6 +323,12 @@ paths: application/json: schema: $ref: '#/components/schemas/Device' + examples: + Example1: + value: + id: 82776 + name: kwyepepbhyjahnafvbmz + type: duxabktuxrqniguywjzn post: operationId: example_CreateDevice tags: @@ -285,6 +341,12 @@ paths: application/json: schema: $ref: '#/components/schemas/Device' + examples: + Example1: + value: + id: 82776 + name: kwyepepbhyjahnafvbmz + type: duxabktuxrqniguywjzn patch: operationId: example_UpdateDevice tags: @@ -317,6 +379,9 @@ paths: required: false schema: type: string + examples: + Example1: + value: created_at,modified_at,id,name - name: $expand in: query required: false @@ -343,6 +408,14 @@ paths: type: array items: $ref: '#/components/schemas/Gateway' + examples: + Example1: + value: + d: + - id: 93907 + name: grwlnjidfweccerzvxgo + - id: 93907 + name: grwlnjidfweccerzvxgo patch: operationId: example_UpdateAllGateway tags: @@ -358,6 +431,9 @@ paths: required: false schema: type: string + examples: + Example1: + value: created_at,modified_at,id,name - name: $expand in: query required: false @@ -391,6 +467,9 @@ paths: required: false schema: type: string + examples: + Example1: + value: created_at,modified_at,id,name - name: $expand in: query required: false @@ -422,6 +501,11 @@ paths: application/json: schema: $ref: '#/components/schemas/Gateway' + examples: + Example1: + value: + id: 93907 + name: grwlnjidfweccerzvxgo post: operationId: example_CreateGateway tags: @@ -434,6 +518,11 @@ paths: application/json: schema: $ref: '#/components/schemas/Gateway' + examples: + Example1: + value: + id: 93907 + name: grwlnjidfweccerzvxgo patch: operationId: example_UpdateGateway tags: @@ -466,6 +555,9 @@ paths: required: false schema: type: string + examples: + Example1: + value: created_at,modified_at,id,gateway,connects__device - name: $expand in: query required: false @@ -492,6 +584,12 @@ paths: type: array items: $ref: '#/components/schemas/Gateway__connects__device' + examples: + Example1: + value: + d: + - id: 58529 + - id: 58529 patch: operationId: example_UpdateAllGateway__connects__device tags: @@ -507,6 +605,9 @@ paths: required: false schema: type: string + examples: + Example1: + value: created_at,modified_at,id,gateway,connects__device - name: $expand in: query required: false @@ -540,6 +641,9 @@ paths: required: false schema: type: string + examples: + Example1: + value: created_at,modified_at,id,gateway,connects__device - name: $expand in: query required: false @@ -571,6 +675,10 @@ paths: application/json: schema: $ref: '#/components/schemas/Gateway__connects__device' + examples: + Example1: + value: + id: 58529 post: operationId: example_CreateGateway__connects__device tags: @@ -583,6 +691,10 @@ paths: application/json: schema: $ref: '#/components/schemas/Gateway__connects__device' + examples: + Example1: + value: + id: 58529 patch: operationId: example_UpdateGateway__connects__device tags: @@ -615,13 +727,13 @@ components: type: integer format: int64 description: The unique identifier for a Application - example: 66373 + example: 4667 name: type: string - example: ttpffnhlhdcvtdofbdel + example: asazpqprbwfzcgqpwggq note: type: string - example: ggewjgbmvhrxrzlquold + example: eqydvcitnsirwajnugcd required: - id - name @@ -640,10 +752,10 @@ components: type: integer format: int64 description: The unique identifier for a Device - example: 50431 + example: 82776 name: type: string - example: ifmxoghvncqheqxwwenr + example: kwyepepbhyjahnafvbmz type: type: string belongs_to__application: @@ -665,7 +777,7 @@ components: type: integer format: int64 description: The unique identifier for a Gateway - example: 83118 + example: 93907 name: type: string required: @@ -684,7 +796,7 @@ components: type: integer format: int64 description: The unique identifier for a Gateway__connects__device - example: 20562 + example: 58529 gateway: $ref: '#/components/schemas/Gateway' connects__device: diff --git a/src/odata-metadata/fern-metadatagenerator.ts b/src/odata-metadata/fern-metadatagenerator.ts index e8db3ad1e..2eb0f181b 100644 --- a/src/odata-metadata/fern-metadatagenerator.ts +++ b/src/odata-metadata/fern-metadatagenerator.ts @@ -189,7 +189,8 @@ export const generateFernMetadata = ( }; const selectableFields: any = []; - const exampleForFields: any = {}; + const exampleForType: any = {}; + const exampleForEndpoint: any = {}; fields .filter(({ dataType }) => dataType !== 'ForeignKey') @@ -214,7 +215,8 @@ export const generateFernMetadata = ( docs: `The unique identifier for a ${capitalize(resourceName)}`, }; } - exampleForFields[fieldName] = exampleFaker(fieldName, dt); + exampleForType[fieldName] = exampleForEndpoint[fieldName] = + exampleFaker(fieldName, dt); }); fields @@ -240,12 +242,12 @@ export const generateFernMetadata = ( ? `optional<${referenceResourceName}>` : referenceResourceName; - // exampleForFields[fieldName] = exampleFaker(fieldName, 'id'); + // exampleForType[fieldName] = exampleFaker(fieldName, 'id'); }); const capitalizedResourceName = capitalize(resourceName); - uniqueTable.examples ??= [{ value: exampleForFields }]; + uniqueTable.examples ??= [{ value: exampleForType }]; fernRootTypes[capitalizedResourceName] = uniqueTable; @@ -286,20 +288,20 @@ export const generateFernMetadata = ( name: capitalize(resKey) + 'all' + capitalizedResourceName, 'query-parameters': ODataQueryParameters, }, - // examples: [ - // { - // 'query-parameters': { - // $select: selectableFields.join(','), - // }, - // response: compileResponse[resKey] - // ? { - // body: { - // d: [exampleForFields, exampleForFields], - // }, - // } - // : undefined, - // }, - // ], + examples: [ + { + 'query-parameters': { + $select: selectableFields.join(','), + }, + response: compileResponse[resKey] + ? { + body: { + d: [exampleForEndpoint, exampleForEndpoint], + }, + } + : undefined, + }, + ], }; } @@ -313,15 +315,15 @@ export const generateFernMetadata = ( request: { name: capitalize(resKey) + capitalizedResourceName + 'ById', }, - // examples: [ - // { - // response: compileResponse[resKey] - // ? { - // body: exampleForFields, - // } - // : undefined, - // }, - // ], + examples: [ + { + response: compileResponse[resKey] + ? { + body: exampleForEndpoint, + } + : undefined, + }, + ], }; } } From be4898174989d5dca5dbe19789bad926251bb0d4 Mon Sep 17 00:00:00 2001 From: fisehara Date: Mon, 3 Apr 2023 16:44:21 +0200 Subject: [PATCH 4/4] combined response type Updated fern to 0.6.6 Signed-off-by: fisehara --- fern/api/definition/example.yaml | 150 ++++++++++----- fern/api/open-api/openapi.yml | 182 ++++++++++++------- fern/fern.config.json | 2 +- package.json | 2 +- src/odata-metadata/fern-metadatagenerator.ts | 41 +++-- 5 files changed, 243 insertions(+), 134 deletions(-) diff --git a/fern/api/definition/example.yaml b/fern/api/definition/example.yaml index d1975a28c..07ab06b83 100644 --- a/fern/api/definition/example.yaml +++ b/fern/api/definition/example.yaml @@ -5,7 +5,7 @@ service: ReadAllApplication: path: /application method: GET - response: map> + response: ApplicationResponse request: name: ReadallApplication query-parameters: @@ -20,12 +20,16 @@ service: response: body: d: - - id: 4667 - name: asazpqprbwfzcgqpwggq - note: eqydvcitnsirwajnugcd - - id: 4667 - name: asazpqprbwfzcgqpwggq - note: eqydvcitnsirwajnugcd + - 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 @@ -35,9 +39,11 @@ service: examples: - response: body: - id: 4667 - name: asazpqprbwfzcgqpwggq - note: eqydvcitnsirwajnugcd + 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 @@ -47,9 +53,11 @@ service: examples: - response: body: - id: 4667 - name: asazpqprbwfzcgqpwggq - note: eqydvcitnsirwajnugcd + 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 @@ -95,7 +103,7 @@ service: ReadAllDevice: path: /device method: GET - response: map> + response: DeviceResponse request: name: ReadallDevice query-parameters: @@ -110,12 +118,16 @@ service: response: body: d: - - id: 82776 - name: kwyepepbhyjahnafvbmz - type: duxabktuxrqniguywjzn - - id: 82776 - name: kwyepepbhyjahnafvbmz - type: duxabktuxrqniguywjzn + - 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 @@ -125,9 +137,11 @@ service: examples: - response: body: - id: 82776 - name: kwyepepbhyjahnafvbmz - type: duxabktuxrqniguywjzn + 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 @@ -137,9 +151,11 @@ service: examples: - response: body: - id: 82776 - name: kwyepepbhyjahnafvbmz - type: duxabktuxrqniguywjzn + 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 @@ -185,7 +201,7 @@ service: ReadAllGateway: path: /gateway method: GET - response: map> + response: GatewayResponse request: name: ReadallGateway query-parameters: @@ -200,10 +216,14 @@ service: response: body: d: - - id: 93907 - name: grwlnjidfweccerzvxgo - - id: 93907 - name: grwlnjidfweccerzvxgo + - 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 @@ -213,8 +233,10 @@ service: examples: - response: body: - id: 93907 - name: grwlnjidfweccerzvxgo + 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 @@ -224,8 +246,10 @@ service: examples: - response: body: - id: 93907 - name: grwlnjidfweccerzvxgo + 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 @@ -271,7 +295,7 @@ service: ReadAllGateway__connects__device: path: /gateway__connects__device method: GET - response: map> + response: Gateway__connects__deviceResponse request: name: ReadallGateway__connects__device query-parameters: @@ -286,8 +310,12 @@ service: response: body: d: - - id: 58529 - - id: 58529 + - 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 @@ -297,7 +325,9 @@ service: examples: - response: body: - id: 58529 + 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 @@ -307,7 +337,9 @@ service: examples: - response: body: - id: 58529 + 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 @@ -366,9 +398,14 @@ types: type: string examples: - value: - id: 4667 - name: asazpqprbwfzcgqpwggq - note: eqydvcitnsirwajnugcd + 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: @@ -385,9 +422,14 @@ types: belongs_to__application: optional examples: - value: - id: 82776 - name: kwyepepbhyjahnafvbmz - type: duxabktuxrqniguywjzn + 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: @@ -401,8 +443,13 @@ types: type: optional examples: - value: - id: 93907 - name: grwlnjidfweccerzvxgo + 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: @@ -416,4 +463,9 @@ types: connects__device: optional examples: - value: - id: 58529 + 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/open-api/openapi.yml b/fern/api/open-api/openapi.yml index 2d0651ec2..f1746a6a4 100644 --- a/fern/api/open-api/openapi.yml +++ b/fern/api/open-api/openapi.yml @@ -43,21 +43,21 @@ paths: content: application/json: schema: - type: object - additionalProperties: - type: array - items: - $ref: '#/components/schemas/Application' + $ref: '#/components/schemas/ApplicationResponse' examples: Example1: value: d: - - id: 4667 - name: asazpqprbwfzcgqpwggq - note: eqydvcitnsirwajnugcd - - id: 4667 - name: asazpqprbwfzcgqpwggq - note: eqydvcitnsirwajnugcd + - 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: @@ -146,9 +146,11 @@ paths: examples: Example1: value: - id: 4667 - name: asazpqprbwfzcgqpwggq - note: eqydvcitnsirwajnugcd + 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: @@ -164,9 +166,11 @@ paths: examples: Example1: value: - id: 4667 - name: asazpqprbwfzcgqpwggq - note: eqydvcitnsirwajnugcd + 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: @@ -223,21 +227,21 @@ paths: content: application/json: schema: - type: object - additionalProperties: - type: array - items: - $ref: '#/components/schemas/Device' + $ref: '#/components/schemas/DeviceResponse' examples: Example1: value: d: - - id: 82776 - name: kwyepepbhyjahnafvbmz - type: duxabktuxrqniguywjzn - - id: 82776 - name: kwyepepbhyjahnafvbmz - type: duxabktuxrqniguywjzn + - 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: @@ -326,9 +330,11 @@ paths: examples: Example1: value: - id: 82776 - name: kwyepepbhyjahnafvbmz - type: duxabktuxrqniguywjzn + 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: @@ -344,9 +350,11 @@ paths: examples: Example1: value: - id: 82776 - name: kwyepepbhyjahnafvbmz - type: duxabktuxrqniguywjzn + 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: @@ -403,19 +411,19 @@ paths: content: application/json: schema: - type: object - additionalProperties: - type: array - items: - $ref: '#/components/schemas/Gateway' + $ref: '#/components/schemas/GatewayResponse' examples: Example1: value: d: - - id: 93907 - name: grwlnjidfweccerzvxgo - - id: 93907 - name: grwlnjidfweccerzvxgo + - 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: @@ -504,8 +512,10 @@ paths: examples: Example1: value: - id: 93907 - name: grwlnjidfweccerzvxgo + 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: @@ -521,8 +531,10 @@ paths: examples: Example1: value: - id: 93907 - name: grwlnjidfweccerzvxgo + 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: @@ -579,17 +591,17 @@ paths: content: application/json: schema: - type: object - additionalProperties: - type: array - items: - $ref: '#/components/schemas/Gateway__connects__device' + $ref: '#/components/schemas/Gateway__connects__deviceResponse' examples: Example1: value: d: - - id: 58529 - - id: 58529 + - 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: @@ -678,7 +690,9 @@ paths: examples: Example1: value: - id: 58529 + 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: @@ -694,7 +708,9 @@ paths: examples: Example1: value: - id: 58529 + 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: @@ -727,17 +743,27 @@ components: type: integer format: int64 description: The unique identifier for a Application - example: 4667 + example: 71065 name: type: string - example: asazpqprbwfzcgqpwggq + example: xrkeejdkpljamasgfarw note: type: string - example: eqydvcitnsirwajnugcd + 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 @@ -752,10 +778,10 @@ components: type: integer format: int64 description: The unique identifier for a Device - example: 82776 + example: 80097 name: type: string - example: kwyepepbhyjahnafvbmz + example: ewgdekqxythufswoytuc type: type: string belongs_to__application: @@ -763,6 +789,16 @@ components: required: - id - name + DeviceResponse: + title: DeviceResponse + type: object + properties: + d: + type: array + items: + $ref: '#/components/schemas/Device' + required: + - d Gateway: title: Gateway type: object @@ -777,11 +813,21 @@ components: type: integer format: int64 description: The unique identifier for a Gateway - example: 93907 + 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 @@ -796,11 +842,21 @@ components: type: integer format: int64 description: The unique identifier for a Gateway__connects__device - example: 58529 + 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 index 3632f9023..42b6a5fa8 100644 --- a/fern/fern.config.json +++ b/fern/fern.config.json @@ -1,4 +1,4 @@ { "organization": "fern", - "version": "0.6.4" + "version": "0.6.6" } \ No newline at end of file diff --git a/package.json b/package.json index aa25e1327..dc50238d0 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "deep-freeze": "^0.0.1", "eventemitter3": "^5.0.0", "express-session": "^1.17.3", - "fern-api": "^0.6.4", + "fern-api": "^0.6.6", "lodash": "^4.17.21", "memoizee": "^0.4.15", "odata-openapi": "^0.21.5", diff --git a/src/odata-metadata/fern-metadatagenerator.ts b/src/odata-metadata/fern-metadatagenerator.ts index 2eb0f181b..4e6334237 100644 --- a/src/odata-metadata/fern-metadatagenerator.ts +++ b/src/odata-metadata/fern-metadatagenerator.ts @@ -140,17 +140,17 @@ export const generateFernMetadata = ( $count: 'optional', }; - type FernEndpoint = { - path: string; - 'path-parameters': { [key: string]: string }; - method: string; - request: { - name: string; - 'query-parameters': typeof ODataQueryParameters; - auth?: boolean; - docs?: string; - }; - }; + // 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 = {}; @@ -160,13 +160,13 @@ export const generateFernMetadata = ( const exampleFaker = ( fieldName: string, dataType?: any, - referencedResource?: string, + // 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 === 'datetime') { + return faker.date.past(); + // return new Date().toISOString(); } else if (dataType === 'string') { return faker.random.alpha(20); } @@ -250,6 +250,9 @@ export const generateFernMetadata = ( 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] @@ -262,8 +265,8 @@ export const generateFernMetadata = ( }; const compileResponse: any = { - read: capitalizedResourceName, - create: capitalizedResourceName, + read: capitalizedResourceName + 'Response', + create: capitalizedResourceName + 'Response', update: undefined, delete: undefined, }; @@ -281,9 +284,7 @@ export const generateFernMetadata = ( ] = { path: `/${resourceName}`, method: httpLookup[resKey], - response: compileResponse[resKey] - ? `map>` - : undefined, + response: compileResponse[resKey], request: { name: capitalize(resKey) + 'all' + capitalizedResourceName, 'query-parameters': ODataQueryParameters,