diff --git a/extensions/sequelize/src/__tests__/fixtures/controllers/user-todo-list.controller.ts b/extensions/sequelize/src/__tests__/fixtures/controllers/user-todo-list.controller.ts index 86884abc2986..cff4e9f99bfe 100644 --- a/extensions/sequelize/src/__tests__/fixtures/controllers/user-todo-list.controller.ts +++ b/extensions/sequelize/src/__tests__/fixtures/controllers/user-todo-list.controller.ts @@ -58,7 +58,7 @@ export class UserTodoListController { schema: getModelSchemaRef(TodoList, { title: 'NewTodoListInUser', exclude: ['id'], - optional: ['user'], + optional: ['userId'], }), }, }, diff --git a/extensions/sequelize/src/__tests__/fixtures/models/patient.model.ts b/extensions/sequelize/src/__tests__/fixtures/models/patient.model.ts index d002ec56c82f..12b48ddf296d 100644 --- a/extensions/sequelize/src/__tests__/fixtures/models/patient.model.ts +++ b/extensions/sequelize/src/__tests__/fixtures/models/patient.model.ts @@ -15,6 +15,12 @@ export class Patient extends Entity { }) name: string; + @property({ + type: 'string', + hidden: true, + }) + password?: string; + constructor(data?: Partial) { super(data); } diff --git a/extensions/sequelize/src/__tests__/fixtures/models/todo-list.model.ts b/extensions/sequelize/src/__tests__/fixtures/models/todo-list.model.ts index 3da047a55ac6..db55e9c043d2 100644 --- a/extensions/sequelize/src/__tests__/fixtures/models/todo-list.model.ts +++ b/extensions/sequelize/src/__tests__/fixtures/models/todo-list.model.ts @@ -1,5 +1,12 @@ -import {Entity, hasMany, model, property} from '@loopback/repository'; +import { + Entity, + hasMany, + model, + property, + belongsTo, +} from '@loopback/repository'; import {Todo} from './todo.model'; +import {User} from './user.model'; @model() export class TodoList extends Entity { @@ -21,10 +28,17 @@ export class TodoList extends Entity { }) todos: Todo[]; - @property({ - type: 'number', - }) - user?: number; + @belongsTo( + () => User, + { + keyTo: 'id', + keyFrom: 'userId', + }, + { + type: 'number', + }, + ) + userId?: number; constructor(data?: Partial) { super(data); diff --git a/extensions/sequelize/src/__tests__/fixtures/models/user.model.ts b/extensions/sequelize/src/__tests__/fixtures/models/user.model.ts index 97a23a6746c2..950c7e52e4a6 100644 --- a/extensions/sequelize/src/__tests__/fixtures/models/user.model.ts +++ b/extensions/sequelize/src/__tests__/fixtures/models/user.model.ts @@ -66,7 +66,7 @@ export class User extends Entity { }) dob?: Date; - @hasOne(() => TodoList, {keyTo: 'user'}) + @hasOne(() => TodoList, {keyTo: 'userId'}) todoList: TodoList; constructor(data?: Partial) { diff --git a/extensions/sequelize/src/__tests__/fixtures/repositories/todo-list.repository.ts b/extensions/sequelize/src/__tests__/fixtures/repositories/todo-list.repository.ts index eb7df4a57f51..a9f87c9e26b4 100644 --- a/extensions/sequelize/src/__tests__/fixtures/repositories/todo-list.repository.ts +++ b/extensions/sequelize/src/__tests__/fixtures/repositories/todo-list.repository.ts @@ -1,9 +1,14 @@ import {Getter, inject} from '@loopback/core'; -import {HasManyRepositoryFactory, repository} from '@loopback/repository'; +import { + HasManyRepositoryFactory, + repository, + type BelongsToAccessor, +} from '@loopback/repository'; import {SequelizeCrudRepository} from '../../../sequelize'; import {PrimaryDataSource} from '../datasources/primary.datasource'; -import {Todo, TodoList, TodoListRelations} from '../models/index'; +import {Todo, TodoList, TodoListRelations, User} from '../models/index'; import {TodoRepository} from './todo.repository'; +import {UserRepository} from './user.repository'; export class TodoListRepository extends SequelizeCrudRepository< TodoList, @@ -15,10 +20,14 @@ export class TodoListRepository extends SequelizeCrudRepository< typeof TodoList.prototype.id >; + public readonly user: BelongsToAccessor; + constructor( @inject('datasources.primary') dataSource: PrimaryDataSource, @repository.getter('TodoRepository') protected todoRepositoryGetter: Getter, + @repository.getter('UserRepository') + protected userRepositoryGetter: Getter, ) { super(TodoList, dataSource); this.todos = this.createHasManyRepositoryFactoryFor( @@ -26,5 +35,8 @@ export class TodoListRepository extends SequelizeCrudRepository< todoRepositoryGetter, ); this.registerInclusionResolver('todos', this.todos.inclusionResolver); + + this.user = this.createBelongsToAccessorFor('user', userRepositoryGetter); + this.registerInclusionResolver('user', this.user.inclusionResolver); } } diff --git a/extensions/sequelize/src/__tests__/integration/repository.integration.ts b/extensions/sequelize/src/__tests__/integration/repository.integration.ts index 9abcdbe0a11a..1d07e5b299f6 100644 --- a/extensions/sequelize/src/__tests__/integration/repository.integration.ts +++ b/extensions/sequelize/src/__tests__/integration/repository.integration.ts @@ -829,7 +829,7 @@ describe('Sequelize CRUD Repository (integration)', () => { const user = getDummyUser(); const userRes = await client.post('/users').send(user); - const todoList = getDummyTodoList({user: userRes.body.id}); + const todoList = getDummyTodoList({userId: userRes.body.id}); const todoListRes = await client.post('/todo-lists').send(todoList); const filter = {include: ['todoList']}; @@ -863,7 +863,7 @@ describe('Sequelize CRUD Repository (integration)', () => { { ...todoListRes.body, todos: [todoRes.body], - user: null, + userId: null, }, ]); }); @@ -898,12 +898,12 @@ describe('Sequelize CRUD Repository (integration)', () => { { ...todoListRes1.body, todos: [todoRes1.body], - user: null, + userId: null, }, { ...todoListRes2.body, todos: [todoRes2.body], - user: null, + userId: null, }, ]); @@ -915,12 +915,12 @@ describe('Sequelize CRUD Repository (integration)', () => { { ...todoListRes2.body, todos: [todoRes2.body], - user: null, + userId: null, }, { ...todoListRes1.body, todos: [todoRes1.body], - user: null, + userId: null, }, ]); @@ -932,12 +932,12 @@ describe('Sequelize CRUD Repository (integration)', () => { { ...todoListRes1.body, todos: [todoRes1.body], - user: null, + userId: null, }, { ...todoListRes2.body, todos: [todoRes2.body], - user: null, + userId: null, }, ]); }); @@ -1005,6 +1005,27 @@ describe('Sequelize CRUD Repository (integration)', () => { ); }); + it('hides hidden properties in related entities included with @belongsTo relation', async () => { + await migrateSchema(['todos', 'todo-lists', 'users']); + + const userRes = await client.post('/users').send(getDummyUser()); + + await client.post('/todo-lists').send( + getDummyTodoList({ + title: 'Todo list one', + userId: userRes.body.id, + }), + ); + + const filter = {include: ['user']}; + const relationRes = await client.get(`/todo-lists`).query({ + filter: JSON.stringify(filter), + }); + + expect(relationRes.body.length).to.be.equal(1); + expect(relationRes.body.at(0).user).not.to.have.property('password'); + }); + it('supports @belongsTo using keyfrom and keyto', async () => { await migrateSchema(['users', 'todos', 'todo-lists']); @@ -1013,13 +1034,13 @@ describe('Sequelize CRUD Repository (integration)', () => { const todoOne = await client.post('/todo-lists').send( getDummyTodoList({ title: 'Todo list one', - user: userRes.body.id, + userId: userRes.body.id, }), ); const todoListRes = await client.post('/todo-lists').send( getDummyTodoList({ title: 'Another todo list', - user: userRes.body.id, + userId: userRes.body.id, }), ); @@ -1057,13 +1078,13 @@ describe('Sequelize CRUD Repository (integration)', () => { const doctorRes = await client.post('/doctors').send(getDummyDoctor()); const patientRes = await client - .post(`/doctors/${1}/patients`) + .post(`/doctors/${doctorRes.body.id}/patients`) .send(getDummyPatient()); const filter = {include: ['patients']}; - const relationRes = await client.get( - `/doctors?filter=${encodeURIComponent(JSON.stringify(filter))}`, - ); + const relationRes = await client + .get(`/doctors`) + .query({filter: JSON.stringify(filter)}); /** * Manually Remove through table data as sqlite3 doesn't support `attributes: []` using sequelize @@ -1078,6 +1099,29 @@ describe('Sequelize CRUD Repository (integration)', () => { ]); }); + it('hides hidden props for nested entities included with @hasMany relation', async () => { + await migrateSchema(['doctors']); + + const doctorRes = await client.post('/doctors').send(getDummyDoctor()); + + await client.post(`/doctors/${doctorRes.body.id}/patients`).send( + getDummyPatient({ + password: 'secret', + }), + ); + + const filter = {include: ['patients']}; + const relationRes = await client + .get(`/doctors`) + .query({filter: JSON.stringify(filter)}); + + expect(relationRes.body.length).to.be.equal(1); + expect(relationRes.body.at(0)).to.have.property('patients'); + expect(relationRes.body.at(0).patients.at(0)).to.not.have.property( + 'password', + ); + }); + it('supports @referencesMany', async () => { await migrateSchema(['developers']); diff --git a/extensions/sequelize/src/sequelize/sequelize.repository.base.ts b/extensions/sequelize/src/sequelize/sequelize.repository.base.ts index 5875dae1dc25..525c36c85e6a 100644 --- a/extensions/sequelize/src/sequelize/sequelize.repository.base.ts +++ b/extensions/sequelize/src/sequelize/sequelize.repository.base.ts @@ -1092,6 +1092,59 @@ export class SequelizeCrudRepository< } } + /** + * Transform related entities queried through relations into their corresponding Loopback models. + * This ensures hidden properties defined in the nested Loopback models are excluded from the response. + * @see https://loopback.io/doc/en/lb4/Model.html#hidden-properties + */ + function transformRelatedEntitiesToLoopbackModels( + entities: T[], + entityClass: typeof Entity, + ) { + entities.forEach(entity => { + for (const key in entityClass.definition.relations) { + const relation = entityClass.definition.relations[key]; + + if (relation && relation.name in entity) { + try { + const TargetLoopbackModel = relation.target(); + const relatedEntityOrEntities = entity[relation.name as keyof T]; + + if (Array.isArray(relatedEntityOrEntities)) { + Object.assign(entity, { + [relation.name]: relatedEntityOrEntities.map( + relatedEntity => { + const safeCopy = {...relatedEntity}; + return new TargetLoopbackModel(safeCopy); + }, + ), + }); + } else { + const safeCopy = {...relatedEntityOrEntities}; + + // Handles belongsTo relation which does not include a list of entities + Object.assign(entity, { + [relation.name]: new TargetLoopbackModel( + safeCopy as DataObject, + ), + }); + } + } catch (error) { + debug( + `Error while transforming relation to Loopback model for relation: ${relation.name}`, + error, + ); + } + } + } + }); + } + + transformRelatedEntitiesToLoopbackModels( + parentEntityInstances, + parentEntityClass, + ); + // Validate data type of items in any column having references // For eg. convert ["1", "2"] into [1, 2] if `itemType` specified is `number[]` parentEntityInstances = parentEntityInstances.map(entity => {