Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand All @@ -1143,7 +1136,7 @@ describe('build-schema', () => {
products: {
type: 'array',
items: {
$ref: '#/definitions/ProductWithRelations',
$ref: '#/definitions/Product',
},
},
},
Expand Down Expand Up @@ -1175,27 +1168,20 @@ 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,
},
},
properties: {
products: {
type: 'array',
items: {$ref: '#/definitions/ProductWithRelations'},
items: {$ref: '#/definitions/Product'},
},
},
additionalProperties: false,
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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'},
},
});
});
Expand Down
1 change: 1 addition & 0 deletions packages/repository-json-schema/src/build-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,7 @@ export function modelToJsonSchema<T extends object>(
// 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}`};
Expand Down