diff --git a/packages/openapi-v3/src/__tests__/unit/json-to-schema.unit.ts b/packages/openapi-v3/src/__tests__/unit/json-to-schema.unit.ts index 94b2e8be6734..e5cad1a74c34 100644 --- a/packages/openapi-v3/src/__tests__/unit/json-to-schema.unit.ts +++ b/packages/openapi-v3/src/__tests__/unit/json-to-schema.unit.ts @@ -39,6 +39,50 @@ describe('jsonToSchemaObject', () => { propertyConversionTest(allOfDef, expectedAllOf); }); + it('convert description with relation to x-relationships', () => { + const descriptionWithRelation: JsonSchema = { + description: + '(tsType: ProductWithRelations, ' + + 'schemaOptions: { includeRelations: true }), ' + + '{"relationships":{"products":{"description":"Category have many Product.","type":"array","items":{"$ref":"#/definitions/ProductWithRelations"},"relationType":"hasMany","foreignKeys":{"categoryId":"Category"}}}}', + }; + type Relationship = { + description?: string; + type?: string; + $ref?: string; + items?: {$ref: string}; + // eslint-disable-next-line @typescript-eslint/naming-convention + 'x-foreign-keys'?: {[x: string]: string}; + // eslint-disable-next-line @typescript-eslint/naming-convention + 'x-relation-type'?: string; + }; + const expectedDescriptionWithRelation: SchemaObject & { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'x-relationships': {[x: string]: Relationship}; + } = { + description: + '(tsType: ProductWithRelations, schemaOptions: { includeRelations: true }), ', + 'x-relationships': { + products: { + description: 'Category have many Product.', + items: { + $ref: '#/definitions/ProductWithRelations', + }, + type: 'array', + 'x-foreign-keys': { + categoryId: 'Category', + }, + 'x-relation-type': 'hasMany', + }, + }, + 'x-typescript-type': 'ProductWithRelations', + }; + propertyConversionTest( + descriptionWithRelation, + expectedDescriptionWithRelation, + ); + }); + it('converts anyOf', () => { const anyOfDef: JsonSchema = { anyOf: [typeDef, typeDef], diff --git a/packages/openapi-v3/src/json-to-schema.ts b/packages/openapi-v3/src/json-to-schema.ts index 3fc49bfe2d17..396f2b7d0d7a 100644 --- a/packages/openapi-v3/src/json-to-schema.ts +++ b/packages/openapi-v3/src/json-to-schema.ts @@ -140,6 +140,22 @@ export function jsonToSchemaObject( delete result[converted]; // Check if the description contains information about TypeScript type const matched = result.description?.match(/^\(tsType: (.+), schemaOptions:/); + if (result.description) { + const relationMatched = result.description.match(/\{"relationships".*$/s); + if (relationMatched) { + const stringifiedRelation = relationMatched[0] + .replace(/foreignKey/g, 'x-foreign-key') + .replace(/relationType/g, 'x-relation-type'); + if (stringifiedRelation) { + result['x-relationships'] = + JSON.parse(stringifiedRelation)['relationships']; + result.description = result.description.replace( + /\{"relationships".*$/s, + '', + ); + } + } + } if (matched) { result['x-typescript-type'] = matched[1]; } diff --git a/packages/repository-json-schema/src/__tests__/integration/build-schema.integration.ts b/packages/repository-json-schema/src/__tests__/integration/build-schema.integration.ts index af512eb78d83..b85420a8bcd8 100644 --- a/packages/repository-json-schema/src/__tests__/integration/build-schema.integration.ts +++ b/packages/repository-json-schema/src/__tests__/integration/build-schema.integration.ts @@ -1126,14 +1126,14 @@ describe('build-schema', () => { type: 'object', description: `(tsType: ProductWithRelations, ` + - `schemaOptions: { includeRelations: true })`, + `schemaOptions: { includeRelations: true }), ` + + `{\"relationships\":{\"category\":{\"description\":\"Product belongs to Category.\",\"type\":\"object\",\"$ref\":\"#/definitions/CategoryWithRelations\",\"foreignKeys\":{\"categoryId\":\"Category\"},\"relationType\":\"belongsTo\"}}}`, properties: { id: {type: 'number'}, categoryId: {type: 'number'}, category: { $ref: '#/definitions/CategoryWithRelations', }, - foreignKey: 'categoryId' as JsonSchema, }, additionalProperties: false, }, @@ -1152,7 +1152,8 @@ describe('build-schema', () => { type: 'object', description: `(tsType: CategoryWithRelations, ` + - `schemaOptions: { includeRelations: true })`, + `schemaOptions: { includeRelations: true }), ` + + `{\"relationships\":{\"products\":{\"description\":\"Category have many Product.\",\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/ProductWithRelations\"},\"foreignKeys\":{\"categoryId\":\"Category\"},\"relationType\":\"hasMany\"}}}`, }; const jsonSchema = getJsonSchema(Category, {includeRelations: true}); expect(jsonSchema).to.deepEqual(expectedSchema); @@ -1180,14 +1181,14 @@ describe('build-schema', () => { type: 'object', description: `(tsType: ProductWithRelations, ` + - `schemaOptions: { includeRelations: true })`, + `schemaOptions: { includeRelations: true }), ` + + `{\"relationships\":{\"category\":{\"description\":\"Product belongs to CategoryWithoutProp.\",\"type\":\"object\",\"$ref\":\"#/definitions/CategoryWithoutPropWithRelations\",\"foreignKeys\":{\"categoryId\":\"CategoryWithoutProp\"},\"relationType\":\"belongsTo\"}}}`, properties: { id: {type: 'number'}, categoryId: {type: 'number'}, category: { $ref: '#/definitions/CategoryWithoutPropWithRelations', }, - foreignKey: 'categoryId' as JsonSchema, }, additionalProperties: false, }, @@ -1203,7 +1204,8 @@ describe('build-schema', () => { type: 'object', description: `(tsType: CategoryWithoutPropWithRelations, ` + - `schemaOptions: { includeRelations: true })`, + `schemaOptions: { includeRelations: true }), ` + + `{\"relationships\":{\"products\":{\"description\":\"CategoryWithoutProp have many Product.\",\"type\":\"array\",\"items\":{\"$ref\":\"#/definitions/ProductWithRelations\"},\"foreignKeys\":{\"categorywithoutpropId\":\"CategoryWithoutProp\"},\"relationType\":\"\hasMany"}}}`, }; // To check for case when there are no other properties than relational diff --git a/packages/repository-json-schema/src/build-schema.ts b/packages/repository-json-schema/src/build-schema.ts index e945000973e8..7a7f22e86cc3 100644 --- a/packages/repository-json-schema/src/build-schema.ts +++ b/packages/repository-json-schema/src/build-schema.ts @@ -577,11 +577,136 @@ export function modelToJsonSchema( result.properties[relMeta.name] = result.properties[relMeta.name] || propDef; - if ((relMeta as {keyFrom: string}).keyFrom) { - result.properties.foreignKey = (relMeta as {keyFrom: string}) - .keyFrom as JsonSchema; + type Relationship = { + description?: string; + type?: string; + $ref?: string; + items?: {$ref: string}; + foreignKeys?: {[x: string]: string}; + relationType?: string; + }; + + const foreignKey: {[x: string]: string} = {}; + let relationships: {[x: string]: Relationship} = {}; + + relationships = {}; + if (!relationships[relMeta.name]) { + relationships[relMeta.name] = {}; } + if (relMeta.type === 'belongsTo') { + let keyFrom = (relMeta as {keyFrom: string}).keyFrom; + if (!keyFrom) { + keyFrom = targetType.name.toLowerCase() + 'Id'; + } + foreignKey[keyFrom] = targetType.name; + relationships[relMeta.name].description = + `${relMeta.source.name} belongs to ${targetType.name}.`; + relationships[relMeta.name].type = 'object'; + relationships[relMeta.name].$ref = + `#/definitions/${targetSchema.title}`; + } + if (relMeta.type === 'hasMany') { + if ((relMeta as {through: object}).through) { + relationships = {}; + if (!relationships[relMeta.name]) { + relationships[relMeta.name] = {}; + } + let keyTo = ( + (relMeta as {through: object}).through as {keyTo: string} + ).keyTo; + let keyFrom = ( + (relMeta as {through: object}).through as {keyFrom: string} + ).keyFrom; + + if (!keyTo) { + keyTo = targetType.name.toLowerCase() + 'Id'; + } + if (!keyFrom) { + keyFrom = relMeta.source.name.toLowerCase() + 'Id'; + } + + foreignKey[keyTo] = targetType.name; + foreignKey[keyFrom] = relMeta.source.name; + + relationships[relMeta.name].description = + `${relMeta.source.name} have many ${targetType.name}.`; + relationships[relMeta.name].type = 'object'; + relationships[relMeta.name].$ref = + `#/definitions/${targetSchema.title}`; + } else { + let keyFrom = (relMeta as {keyFrom: string}).keyFrom; + if (!keyFrom) { + keyFrom = relMeta.source.name.toLowerCase() + 'Id'; + } + foreignKey[keyFrom] = relMeta.source.name; + relationships[relMeta.name].description = + `${relMeta.source.name} have many ${targetType.name}.`; + relationships[relMeta.name].type = 'array'; + relationships[relMeta.name].items = { + $ref: `#/definitions/${targetSchema.title}`, + }; + } + } + + if (relMeta.type === 'hasOne') { + relationships = {}; + if (!relationships[relMeta.name]) { + relationships[relMeta.name] = {}; + } + + let keyTo = (relMeta as {keyTo: string}).keyTo; + + if (!keyTo) { + keyTo = relMeta.source.name.toLowerCase() + 'Id'; + } + foreignKey[keyTo] = relMeta.source.name; + relationships[relMeta.name].description = + `${relMeta.source.name} have one ${targetType.name}.`; + relationships[relMeta.name].type = 'object'; + relationships[relMeta.name].$ref = + `#/definitions/${targetSchema.title}`; + } + + if (relMeta.type === 'referencesMany') { + let keyFrom = (relMeta as {keyFrom: string}).keyFrom; + if (!keyFrom) { + keyFrom = targetType.name.toLowerCase() + 'Ids'; + } + foreignKey[keyFrom] = targetType.name; + relationships[relMeta.name].description = + `${relMeta.source.name} references many ${targetType.name}.`; + relationships[relMeta.name].type = 'array'; + relationships[relMeta.name].items = { + $ref: `#/definitions/${targetSchema.title}`, + }; + } + relationships[relMeta.name].foreignKeys = foreignKey; + relationships[relMeta.name].relationType = relMeta.type; + if (result.description) { + if (result.description.includes('relationships')) { + const relationMatched = + result.description.match(/\{"relationships".*$/s); + if (relationMatched) { + const {relationships: existingRelation} = JSON.parse( + relationMatched[0], + ); + existingRelation[Object.keys(relationships)[0]] = { + ...relationships, + }; + result.description = result.description.replace( + /\{"relationships".*$/s, + '', + ); + result.description = + result.description + + `, ${JSON.stringify({relationships: existingRelation})}`; + } + } else { + result.description = + result.description + `, ${JSON.stringify({relationships})}`; + } + } includeReferencedSchema(targetSchema.title!, targetSchema); } }