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..06788f13e837 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 @@ -1121,19 +1121,12 @@ describe('build-schema', () => { const expectedSchema: JsonSchema = { definitions: { - ProductWithRelations: { - title: 'ProductWithRelations', + Product: { + title: 'Product', type: 'object', - description: - `(tsType: ProductWithRelations, ` + - `schemaOptions: { includeRelations: true })`, properties: { id: {type: 'number'}, categoryId: {type: 'number'}, - category: { - $ref: '#/definitions/CategoryWithRelations', - }, - foreignKey: 'categoryId' as JsonSchema, }, additionalProperties: false, }, @@ -1143,7 +1136,7 @@ describe('build-schema', () => { products: { type: 'array', items: { - $ref: '#/definitions/ProductWithRelations', + $ref: '#/definitions/Product', }, }, }, @@ -1175,19 +1168,12 @@ describe('build-schema', () => { } const expectedSchema: JsonSchema = { definitions: { - ProductWithRelations: { - title: 'ProductWithRelations', + Product: { + title: 'Product', type: 'object', - description: - `(tsType: ProductWithRelations, ` + - `schemaOptions: { includeRelations: true })`, properties: { id: {type: 'number'}, categoryId: {type: 'number'}, - category: { - $ref: '#/definitions/CategoryWithoutPropWithRelations', - }, - foreignKey: 'categoryId' as JsonSchema, }, additionalProperties: false, }, @@ -1195,7 +1181,7 @@ describe('build-schema', () => { properties: { products: { type: 'array', - items: {$ref: '#/definitions/ProductWithRelations'}, + items: {$ref: '#/definitions/Product'}, }, }, additionalProperties: false, @@ -1213,6 +1199,52 @@ describe('build-schema', () => { expect(jsonSchemaWithoutProp).to.deepEqual(expectedSchema); }); + it('does not produce circular $ref chains for bidirectional relations', () => { + @model() + class Order extends Entity { + @property({id: true}) + id: number; + + @belongsTo(() => Customer) + customerId: number; + } + + @model() + class Customer extends Entity { + @property({id: true}) + id: number; + + @hasMany(() => Order) + orders?: Order[]; + } + + const schema = getJsonSchema(Customer, {includeRelations: true}); + + // orders items should reference plain Order, not OrderWithRelations + expect(schema.properties).to.containDeep({ + orders: { + type: 'array', + items: {$ref: '#/definitions/Order'}, + }, + }); + + // The Order definition should be plain — no nested definitions, + // no customer navigational property + const orderDef = schema.definitions?.Order as JsonSchema; + expect(orderDef).to.be.ok(); + expect(orderDef.definitions).to.be.undefined(); + expect(orderDef.properties).to.not.have.property('customer'); + + // No WithRelations keys should appear in definitions + const defKeys = Object.keys(schema.definitions ?? {}); + for (const key of defKeys) { + expect(key).to.not.match( + /WithRelations/, + `definitions should not contain WithRelations keys, found: ${key}`, + ); + } + }); + it('gets cached JSON schema with relation links if one exists', () => { @model() class Product extends Entity { diff --git a/packages/repository-json-schema/src/__tests__/unit/build-schema.unit.ts b/packages/repository-json-schema/src/__tests__/unit/build-schema.unit.ts index 29493866baf2..2c5068c69e91 100644 --- a/packages/repository-json-schema/src/__tests__/unit/build-schema.unit.ts +++ b/packages/repository-json-schema/src/__tests__/unit/build-schema.unit.ts @@ -325,30 +325,27 @@ describe('build-schema', () => { expect(schema.properties).to.containEql({ children: { type: 'array', - // The reference here should be `ChildWithRelations`, - // instead of `ParentWithItsChildren` - items: {$ref: '#/definitions/ChildWithRelations'}, + // The reference here should be `Child` (not `ParentWithItsChildren`) + // because includeRelations is not cascaded to relation targets. + items: {$ref: '#/definitions/Child'}, }, benchmarkId: {type: 'string'}, color: {type: 'string'}, }); - // The recursive calls should NOT inherit - // `title` from the previous call's `options`. - // So the `title` here is `ChildWithRelations` - // instead of `ParentWithItsChildren`. + // The recursive calls should NOT inherit `title` from the previous + // call's `options`, and `includeRelations` is not propagated to + // relation targets. So the definition is plain `Child`. expect(schema.definitions).to.containEql({ - ChildWithRelations: { - title: 'ChildWithRelations', + Child: { + title: 'Child', type: 'object', - description: - '(tsType: ChildWithRelations, schemaOptions: { includeRelations: true })', properties: {name: {type: 'string'}}, additionalProperties: false, }, }); }); - it('includeRelations is not propagated to properties decorated with @property()', () => { + it('includeRelations is not propagated to nested properties or relation targets', () => { @model() class FooModel extends Entity { @property({ @@ -379,14 +376,14 @@ describe('build-schema', () => { name: { type: 'string', }, - // Decorated with @property() Model should NOT be 'WithRelations' + // Decorated with @property() — no includeRelations propagation foo: { $ref: '#/definitions/FooModel', }, - // Decorated with @hasMany() Model should be 'WithRelations' + // Decorated with @hasMany() — also no includeRelations propagation relatedFoo: { type: 'array', - items: {$ref: '#/definitions/FooModelWithRelations'}, + items: {$ref: '#/definitions/FooModel'}, }, }); }); diff --git a/packages/repository-json-schema/src/build-schema.ts b/packages/repository-json-schema/src/build-schema.ts index e945000973e8..733a4a538f6d 100644 --- a/packages/repository-json-schema/src/build-schema.ts +++ b/packages/repository-json-schema/src/build-schema.ts @@ -570,6 +570,7 @@ export function modelToJsonSchema( // when generating the relation or property schemas const targetOptions = {...options}; delete targetOptions.title; + delete targetOptions.includeRelations; const targetSchema = getJsonSchema(targetType, targetOptions); const targetRef = {$ref: `#/definitions/${targetSchema.title}`};