diff --git a/packages/repository/src/__tests__/unit/repositories/legacy-juggler-bridge.unit.ts b/packages/repository/src/__tests__/unit/repositories/legacy-juggler-bridge.unit.ts index ee7d4ac1dee7..63de1a675e16 100644 --- a/packages/repository/src/__tests__/unit/repositories/legacy-juggler-bridge.unit.ts +++ b/packages/repository/src/__tests__/unit/repositories/legacy-juggler-bridge.unit.ts @@ -210,6 +210,35 @@ describe('DefaultCrudRepository', () => { expect(User.definition.properties.roles.itemType).to.equal(Role); expect(User.definition.properties.address.type).to.equal(Address); }); + + it('handles recursive model references', () => { + @model() + class ReportState extends Entity { + @property({id: true}) + id: string; + + @property.array(ReportState, {}) + states: ReportState[]; + + @property({ + type: 'string', + }) + benchmarkId?: string; + + @property({ + type: 'string', + }) + color?: string; + + constructor(data?: Partial) { + super(data); + } + } + const repo = new DefaultCrudRepository(ReportState, ds); + const definition = repo.modelClass.definition; + const typeOfStates = definition.properties.states.type; + expect(typeOfStates).to.eql([repo.modelClass]); + }); }); it('shares the backing PersistedModel across repo instances', () => { diff --git a/packages/repository/src/repositories/legacy-juggler-bridge.ts b/packages/repository/src/repositories/legacy-juggler-bridge.ts index 5606125d0129..3fa02d4fa6cc 100644 --- a/packages/repository/src/repositories/legacy-juggler-bridge.ts +++ b/packages/repository/src/repositories/legacy-juggler-bridge.ts @@ -127,12 +127,13 @@ export class DefaultCrudRepository< `Entity ${entityClass.name} must have at least one id/pk property.`, ); - this.modelClass = this.definePersistedModel(entityClass); + this.modelClass = this.definePersistedModel(entityClass, new Map()); } // Create an internal legacy Model attached to the datasource private definePersistedModel( entityClass: typeof Model, + visited: Map, ): typeof juggler.PersistedModel { const definition = entityClass.definition; assert( @@ -140,18 +141,41 @@ export class DefaultCrudRepository< `Entity ${entityClass.name} must have valid model definition.`, ); + let resolved = visited.get(entityClass); + if (resolved) { + return resolved; + } + const dataSource = this.dataSource; const model = dataSource.getModel(definition.name); if (model) { // The backing persisted model has been already defined. - return model as typeof juggler.PersistedModel; + resolved = model as typeof juggler.PersistedModel; + visited.set(entityClass, resolved); + return resolved; } // We need to convert property definitions from PropertyDefinition // to plain data object because of a juggler limitation const properties: {[name: string]: object} = {}; + const modelClass = dataSource.createModel( + definition.name, + definition.properties, + Object.assign( + // settings that users can override + {strict: true}, + // user-defined settings + definition.settings, + // settings enforced by the framework + {strictDelete: false}, + ), + ); + + // Cache the result to allow recursive refs + visited.set(entityClass, modelClass); + // We need to convert PropertyDefinition into the definition that // the juggler understands Object.entries(definition.properties).forEach(([key, value]) => { @@ -159,36 +183,37 @@ export class DefaultCrudRepository< // ensures that model definitions can be reused with multiple datasources if (value.type === 'array' || value.type === Array) { value = Object.assign({}, value, { - type: [value.itemType && this.resolvePropertyType(value.itemType)], + type: [ + value.itemType && this.resolvePropertyType(value.itemType, visited), + ], }); delete value.itemType; } else { value = Object.assign({}, value, { - type: this.resolvePropertyType(value.type), + type: this.resolvePropertyType(value.type, visited), }); } properties[key] = Object.assign({}, value); }); - const modelClass = dataSource.createModel( - definition.name, - properties, - Object.assign( - // settings that users can override - {strict: true}, - // user-defined settings - definition.settings, - // settings enforced by the framework - {strictDelete: false}, - ), - ); + + // Now the property types have been fully resolved + // Force the model to be rebuilt + modelClass.definition.rawProperties = properties; + delete modelClass.definition.properties; + + // Force rebuild definitions + modelClass.definition.build(); modelClass.attachTo(dataSource); return modelClass; } - private resolvePropertyType(type: PropertyType): PropertyType { + private resolvePropertyType( + type: PropertyType, + visited: Map, + ): PropertyType { const resolved = resolveType(type); return isModelClass(resolved) - ? this.definePersistedModel(resolved) + ? this.definePersistedModel(resolved, visited) : resolved; }