Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(sequelize): ensure nested relations follow hidden property configuration from Loopback entities #10779

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
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 @@ -58,7 +58,7 @@ export class UserTodoListController {
schema: getModelSchemaRef(TodoList, {
title: 'NewTodoListInUser',
exclude: ['id'],
optional: ['user'],
optional: ['userId'],
}),
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ export class Patient extends Entity {
})
name: string;

@property({
type: 'string',
hidden: true,
})
password?: string;

constructor(data?: Partial<Patient>) {
super(data);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<TodoList>) {
super(data);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class User extends Entity {
})
dob?: Date;

@hasOne(() => TodoList, {keyTo: 'user'})
@hasOne(() => TodoList, {keyTo: 'userId'})
todoList: TodoList;

constructor(data?: Partial<User>) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -15,16 +20,23 @@ export class TodoListRepository extends SequelizeCrudRepository<
typeof TodoList.prototype.id
>;

public readonly user: BelongsToAccessor<User, typeof TodoList.prototype.id>;

constructor(
@inject('datasources.primary') dataSource: PrimaryDataSource,
@repository.getter('TodoRepository')
protected todoRepositoryGetter: Getter<TodoRepository>,
@repository.getter('UserRepository')
protected userRepositoryGetter: Getter<UserRepository>,
) {
super(TodoList, dataSource);
this.todos = this.createHasManyRepositoryFactoryFor(
'todos',
todoRepositoryGetter,
);
this.registerInclusionResolver('todos', this.todos.inclusionResolver);

this.user = this.createBelongsToAccessorFor('user', userRepositoryGetter);
this.registerInclusionResolver('user', this.user.inclusionResolver);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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']};
Expand Down Expand Up @@ -863,7 +863,7 @@ describe('Sequelize CRUD Repository (integration)', () => {
{
...todoListRes.body,
todos: [todoRes.body],
user: null,
userId: null,
},
]);
});
Expand Down Expand Up @@ -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,
},
]);

Expand All @@ -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,
},
]);

Expand All @@ -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,
},
]);
});
Expand Down Expand Up @@ -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']);

Expand All @@ -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,
}),
);

Expand Down Expand Up @@ -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
Expand All @@ -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']);

Expand Down
53 changes: 53 additions & 0 deletions extensions/sequelize/src/sequelize/sequelize.repository.base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Model>,
),
});
}
} 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 => {
Expand Down
Loading