Skip to content
This repository has been archived by the owner on Sep 4, 2024. It is now read-only.

Commit

Permalink
fix(repository): defer to toJSON for hidden property exclusion (#39)
Browse files Browse the repository at this point in the history
Previously, the package used a custom `excludeHiddenProps` function to exclude hidden fields.
Which prevented users from accessing those fields on backend.
With this change, we now defer to the LoopBack Model's built-in toJSON method,
which already handles the exclusion of hidden properties when serializing data for a response.

GH-37
  • Loading branch information
shubhamp-sf committed Jun 13, 2023
1 parent fa3cd07 commit 7e36924
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 23 deletions.
8 changes: 8 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"@semantic-release/git": "^10.0.1",
"@semantic-release/npm": "^9.0.2",
"@semantic-release/release-notes-generator": "^10.0.3",
"@types/lodash": "^4.14.195",
"@types/node": "^14.18.36",
"commitizen": "^4.3.0",
"cz-conventional-changelog": "^3.3.0",
Expand All @@ -79,6 +80,7 @@
"git-release-notes": "^5.0.0",
"husky": "^8.0.3",
"jsdom": "^21.1.0",
"lodash": "^4.17.21",
"pg": "^8.8.0",
"pg-hstore": "^2.3.4",
"semantic-release": "^19.0.5",
Expand Down
6 changes: 6 additions & 0 deletions src/__tests__/fixtures/models/developer.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ export class Developer extends Entity {
)
programmingLanguageIds: number[];

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

constructor(data?: Partial<Developer>) {
super(data);
}
Expand Down
6 changes: 6 additions & 0 deletions src/__tests__/fixtures/models/programming-language.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ export class ProgrammingLanguage extends Entity {
})
name: string;

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

constructor(data?: Partial<ProgrammingLanguage>) {
super(data);
}
Expand Down
6 changes: 6 additions & 0 deletions src/__tests__/fixtures/models/user.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ export class User extends Entity {
})
dob?: Date;

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

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

Expand Down
160 changes: 151 additions & 9 deletions src/__tests__/integration/repository.integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,20 @@ import {
StubbedInstanceWithSinonAccessor,
TestSandbox,
} from '@loopback/testlab';
import _ from 'lodash';
import {resolve} from 'path';
import {UniqueConstraintError} from 'sequelize';
import {SequelizeCrudRepository, SequelizeDataSource} from '../../sequelize';
import {SequelizeSandboxApplication} from '../fixtures/application';
import {config as primaryDataSourceConfig} from '../fixtures/datasources/primary.datasource';
import {config as secondaryDataSourceConfig} from '../fixtures/datasources/secondary.datasource';
import {TableInSecondaryDB} from '../fixtures/models';
import {ProgrammingLanguage, TableInSecondaryDB} from '../fixtures/models';
import {Box, Event, eventTableName} from '../fixtures/models/test.model';
import {UserRepository} from '../fixtures/repositories';
import {
DeveloperRepository,
ProgrammingLanguageRepository,
UserRepository,
} from '../fixtures/repositories';

type Entities =
| 'users'
Expand All @@ -35,6 +40,8 @@ describe('Sequelize CRUD Repository (integration)', function () {

let app: SequelizeSandboxApplication;
let userRepo: UserRepository;
let developerRepo: DeveloperRepository;
let languagesRepo: ProgrammingLanguageRepository;
let client: Client;
let datasource: StubbedInstanceWithSinonAccessor<SequelizeDataSource>;

Expand Down Expand Up @@ -74,7 +81,83 @@ describe('Sequelize CRUD Repository (integration)', function () {
it('creates an entity', async () => {
const user = getDummyUser();
const createResponse = await client.post('/users').send(user);
expect(createResponse.body).deepEqual({id: 1, ...user});
expect(createResponse.body).deepEqual({
id: 1,
..._.omit(user, ['password']),
});
});

it('returns model data without hidden props in response of create', async () => {
const user = getDummyUser();
const createResponse = await client.post('/users').send(user);
expect(createResponse.body).not.to.have.property('password');
});

it('[create] allows accessing hidden props before serializing', async () => {
const user = getDummyUser();
const userData = await userRepo.create({
name: user.name,
address: user.address as AnyObject,
email: user.email,
password: user.password,
dob: user.dob,
active: user.active,
});

expect(userData).to.have.property('password');
expect(userData.password).to.be.eql(user.password);
const afterResponse = userData.toJSON();
expect(afterResponse).to.not.have.property('password');
});

it('[find] allows accessing hidden props before serializing', async () => {
const user = getDummyUser();
await userRepo.create({
name: user.name,
address: user.address as AnyObject,
email: user.email,
password: user.password,
dob: user.dob,
active: user.active,
});

const userData = await userRepo.find();

expect(userData[0]).to.have.property('password');
expect(userData[0].password).to.be.eql(user.password);
const afterResponse = userData[0].toJSON();
expect(afterResponse).to.not.have.property('password');
});

it('[findById] allows accessing hidden props before serializing', async () => {
const user = getDummyUser();
const createdUser = await userRepo.create({
name: user.name,
address: user.address as AnyObject,
email: user.email,
password: user.password,
dob: user.dob,
active: user.active,
});

const userData = await userRepo.findById(createdUser.id);

expect(userData).to.have.property('password');
expect(userData.password).to.be.eql(user.password);
const afterResponse = userData.toJSON();
expect(afterResponse).to.not.have.property('password');
});

it('creates an entity and finds it', async () => {
const user = getDummyUser();
await client.post('/users').send(user);
const userResponse = await client.get('/users').send();
expect(userResponse.body).deepEqual([
{
id: 1,
..._.omit(user, ['password']),
},
]);
});

it('counts created entities', async () => {
Expand Down Expand Up @@ -173,15 +256,23 @@ describe('Sequelize CRUD Repository (integration)', function () {
delete user.active;

await userRepo.execute(
'INSERT INTO "user" (name, email, is_active, address, dob) VALUES ($1, $2, $3, $4, $5)',
[user.name, user.email, user.is_active, user.address, user.dob],
'INSERT INTO "user" (name, email, password, is_active, address, dob) VALUES ($1, $2, $3, $4, $5, $6)',
[
user.name,
user.email,
user.password,
user.is_active,
user.address,
user.dob,
],
);

const users = await userRepo.execute('SELECT * from "user"');

expect(users).to.have.length(1);
expect(users[0]).property('name').to.be.eql(user.name);
expect(users[0]).property('email').to.be.eql(user.email);
expect(users[0]).property('password').to.be.eql(user.password);
expect(users[0]).property('address').to.be.eql(user.address);
expect(new Date(users[0].dob)).to.be.eql(new Date(user.dob!));
expect(users[0]).property('is_active').to.be.ok();
Expand All @@ -200,7 +291,7 @@ describe('Sequelize CRUD Repository (integration)', function () {
delete user.active;

await userRepo.execute(
'INSERT INTO "user" (name, email, is_active, address, dob) VALUES ($name, $email, $is_active, $address, $dob)',
'INSERT INTO "user" (name, email, password, is_active, address, dob) VALUES ($name, $email, $password, $is_active, $address, $dob)',
user,
);

Expand All @@ -209,6 +300,7 @@ describe('Sequelize CRUD Repository (integration)', function () {
expect(users).to.have.length(1);
expect(users[0]).property('name').to.be.eql(user.name);
expect(users[0]).property('email').to.be.eql(user.email);
expect(users[0]).property('password').to.be.eql(user.password);
expect(users[0]).property('address').to.be.eql(user.address);
expect(new Date(users[0].dob)).to.be.eql(new Date(user.dob!));
expect(users[0]).property('is_active').to.be.ok();
Expand Down Expand Up @@ -411,16 +503,17 @@ describe('Sequelize CRUD Repository (integration)', function () {
await migrateSchema(['developers']);

const programmingLanguages = [
getDummyProgrammingLanguage({name: 'JS'}),
getDummyProgrammingLanguage({name: 'Java'}),
getDummyProgrammingLanguage({name: 'Dot Net'}),
getDummyProgrammingLanguage({name: 'JS', secret: 'Practice'}),
getDummyProgrammingLanguage({name: 'Java', secret: 'Practice'}),
getDummyProgrammingLanguage({name: 'Dot Net', secret: 'Practice'}),
];
const createAllResponse = await client
.post('/programming-languages-bulk')
.send(programmingLanguages);

const createDeveloperResponse = await client.post('/developers').send(
getDummyDeveloper({
apiSecret: 'xyz-123-abcd',
programmingLanguageIds: createAllResponse.body.map(
(language: {id: number}) => language.id,
),
Expand Down Expand Up @@ -451,6 +544,51 @@ describe('Sequelize CRUD Repository (integration)', function () {
});
});

it('hides hidden props for nested entities included with referencesMany relation', async () => {
await developerRepo.syncLoadedSequelizeModels({force: true});

const programmingLanguages = [
getDummyProgrammingLanguage({name: 'JS', secret: 'woo'}),
getDummyProgrammingLanguage({name: 'Java', secret: 'woo'}),
getDummyProgrammingLanguage({name: 'Dot Net', secret: 'woo'}),
];

const createAllResponse = await languagesRepo.createAll(
programmingLanguages,
);
expect(createAllResponse[0]).to.have.property('secret');

const createDeveloperResponse = await developerRepo.create(
getDummyDeveloper({
apiSecret: 'xyz-123-abcd',
programmingLanguageIds: createAllResponse.map(
(language: ProgrammingLanguage) => language.id,
),
}),
);

const filter = {include: ['programmingLanguages']};
const relationRes = await developerRepo.findById(
createDeveloperResponse.id,
filter,
);

if (primaryDataSourceConfig.connector === 'sqlite3') {
/**
* sqlite3 doesn't support array data type using it will convert values
* to comma saperated string
*/
createDeveloperResponse.programmingLanguageIds =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
createDeveloperResponse.programmingLanguageIds.join(',') as any;
}

expect(relationRes.toJSON()).to.be.deepEqual({
...createDeveloperResponse.toJSON(),
programmingLanguages: createAllResponse.map(e => e.toJSON()),
});
});

it('supports INNER JOIN', async () => {
await migrateSchema(['books']);

Expand Down Expand Up @@ -715,6 +853,8 @@ describe('Sequelize CRUD Repository (integration)', function () {
await app.start();

userRepo = await app.getRepository(UserRepository);
developerRepo = await app.getRepository(DeveloperRepository);
languagesRepo = await app.getRepository(ProgrammingLanguageRepository);
datasource = createStubInstance(SequelizeDataSource);
client = createRestAppClient(app as RestApplication);
}
Expand All @@ -728,6 +868,7 @@ describe('Sequelize CRUD Repository (integration)', function () {
email: string;
active?: boolean;
address: AnyObject | string;
password?: string;
dob: Date | string;
} & AnyObject;

Expand All @@ -736,6 +877,7 @@ describe('Sequelize CRUD Repository (integration)', function () {
email: '[email protected]',
active: true,
address: {city: 'Indore', zipCode: 452001},
password: 'secret',
dob: timestamp,
...overwrite,
};
Expand Down
Loading

0 comments on commit 7e36924

Please sign in to comment.