Skip to content

Commit fde4744

Browse files
KalleVsamarpanB
authored andcommitted
fix(sequelize): add type coercion for booleans in the Where filter
Cast entity columns with type "boolean" from strings to boolean values in the Loopback 4 Where Filter to Sequelize operation translation layer. Signed-off-by: KalleV <[email protected]>
1 parent 2ebba10 commit fde4744

File tree

3 files changed

+105
-5
lines changed

3 files changed

+105
-5
lines changed

extensions/sequelize/src/__tests__/integration/repository.integration.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,73 @@ describe('Sequelize CRUD Repository (integration)', () => {
280280
]);
281281
});
282282

283+
it('creates an entity and finds it using Where filter boolean conditions', async () => {
284+
const user = getDummyUser();
285+
286+
await client.post('/users').send(user);
287+
288+
const expectedUser = {
289+
id: 1,
290+
..._.omit(user, ['password']),
291+
};
292+
293+
const userResponse1 = await client
294+
.get('/users')
295+
.query({
296+
filter: {
297+
where: {
298+
active: true,
299+
},
300+
},
301+
})
302+
.send();
303+
304+
expect(userResponse1.body).deepEqual([expectedUser]);
305+
306+
const userResponse2 = await client
307+
.get('/users')
308+
.query({
309+
filter: {
310+
where: {
311+
active: false,
312+
},
313+
},
314+
})
315+
.send();
316+
317+
expect(userResponse2.body).deepEqual([]);
318+
319+
const userResponse3 = await client
320+
.get('/users')
321+
.query({
322+
filter: {
323+
where: {
324+
active: {
325+
eq: true,
326+
},
327+
},
328+
},
329+
})
330+
.send();
331+
332+
expect(userResponse3.body).deepEqual([expectedUser]);
333+
334+
const userResponse4 = await client
335+
.get('/users')
336+
.query({
337+
filter: {
338+
where: {
339+
active: {
340+
eq: false,
341+
},
342+
},
343+
},
344+
})
345+
.send();
346+
347+
expect(userResponse4.body).deepEqual([]);
348+
});
349+
283350
it('counts created entities', async () => {
284351
await client.post('/users').send(getDummyUser());
285352
const getResponse = await client.get('/users/count').send();

extensions/sequelize/src/sequelize/sequelize.repository.base.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ import {MakeNullishOptional} from 'sequelize/types/utils';
6262
import {operatorTranslations} from './operator-translation';
6363
import {SequelizeDataSource} from './sequelize.datasource.base';
6464
import {SequelizeModel} from './sequelize.model';
65-
import {isTruelyObject} from './utils';
65+
import {castToBoolean, isTruelyObject} from './utils';
6666

6767
const debug = debugFactory('loopback:sequelize:repository');
6868
const debugModelBuilder = debugFactory('loopback:sequelize:modelbuilder');
@@ -646,15 +646,27 @@ export class SequelizeCrudRepository<
646646
continue;
647647
}
648648

649+
const entityClassCol = this.entityClass.definition.properties[columnName];
650+
const isBooleanColumn =
651+
entityClassCol && entityClassCol.type === 'boolean';
652+
649653
if (isTruelyObject(conditionValue)) {
650654
sequelizeWhere[columnName] = {};
651655

652656
for (const lb4Operator of Object.keys(<Object>conditionValue)) {
653657
const sequelizeOperator = this.getSequelizeOperator(
654658
lb4Operator as keyof typeof operatorTranslations,
655659
);
656-
sequelizeWhere[columnName][sequelizeOperator] =
657-
conditionValue![lb4Operator as keyof typeof conditionValue];
660+
661+
if (isBooleanColumn) {
662+
// Handles boolean column conditions like `{ neq: true }` or `{ eq: false }`
663+
sequelizeWhere[columnName][sequelizeOperator] = castToBoolean(
664+
conditionValue![lb4Operator as keyof typeof conditionValue],
665+
);
666+
} else {
667+
sequelizeWhere[columnName][sequelizeOperator] =
668+
conditionValue![lb4Operator as keyof typeof conditionValue];
669+
}
658670
}
659671
} else if (
660672
['and', 'or'].includes(columnName) &&
@@ -673,9 +685,12 @@ export class SequelizeCrudRepository<
673685
[sequelizeOperator]: conditions,
674686
});
675687
} else {
676-
// Equals
688+
// Equals operation. Casting boolean columns to avoid passing strings as boolean values
689+
// to the Sequelize query builders.
677690
sequelizeWhere[columnName] = {
678-
[Op.eq]: conditionValue,
691+
[Op.eq]: isBooleanColumn
692+
? castToBoolean(conditionValue)
693+
: conditionValue,
679694
};
680695
}
681696
}

extensions/sequelize/src/sequelize/utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,21 @@
1111
export const isTruelyObject = (value?: unknown) => {
1212
return typeof value === 'object' && !Array.isArray(value) && value !== null;
1313
};
14+
15+
/**
16+
* Coerces a value to a boolean. This is used for Where Filter type coercion
17+
* to avoid passing "true" or "false" as strings to the internal Sequelize queries.
18+
*
19+
* @param value - The value to be serialized.
20+
* @returns The coerced boolean value: true / false.
21+
*/
22+
export const castToBoolean = (value: unknown): boolean => {
23+
if (typeof value === 'boolean') {
24+
return value;
25+
} else if (typeof value === 'string') {
26+
return value.toLowerCase() === 'true';
27+
} else {
28+
// e.g. null, undefined, 0, 1, etc.
29+
return Boolean(value);
30+
}
31+
};

0 commit comments

Comments
 (0)