diff --git a/packages/core/database/src/__tests__/operator/string-operator.test.ts b/packages/core/database/src/__tests__/operator/string-operator.test.ts index d3bc960d7..696d1cba0 100644 --- a/packages/core/database/src/__tests__/operator/string-operator.test.ts +++ b/packages/core/database/src/__tests__/operator/string-operator.test.ts @@ -24,6 +24,22 @@ describe('string operator', () => { }); }); + it('should escape underscore in inlcude operator', async () => { + const u1 = await db.getRepository('users').create({ + values: { + name: 'names of u1', + }, + }); + + const u1Res = await db.getRepository('users').findOne({ + filter: { + 'name.$includes': '_', + }, + }); + + expect(u1Res).toBeNull(); + }); + it('should query with include operator', async () => { const u1 = await db.getRepository('users').create({ values: { diff --git a/packages/core/database/src/operators/string.ts b/packages/core/database/src/operators/string.ts index c87a34b73..4c688ebd6 100644 --- a/packages/core/database/src/operators/string.ts +++ b/packages/core/database/src/operators/string.ts @@ -1,11 +1,15 @@ import { Op } from 'sequelize'; import { isPg } from './utils'; +function escapeLike(value: string) { + return value.replace(/[_%]/g, '\\$&'); +} + export default { $includes(value, ctx) { if (Array.isArray(value)) { const conditions = value.map((item) => ({ - [isPg(ctx) ? Op.iLike : Op.like]: `%${item}%`, + [isPg(ctx) ? Op.iLike : Op.like]: `%${escapeLike(item)}%`, })); return { @@ -14,14 +18,14 @@ export default { } return { - [isPg(ctx) ? Op.iLike : Op.like]: `%${value}%`, + [isPg(ctx) ? Op.iLike : Op.like]: `%${escapeLike(value)}%`, }; }, $notIncludes(value, ctx) { if (Array.isArray(value)) { const conditions = value.map((item) => ({ - [isPg(ctx) ? Op.notILike : Op.notLike]: `%${item}%`, + [isPg(ctx) ? Op.notILike : Op.notLike]: `%${escapeLike(item)}%`, })); return { @@ -30,14 +34,14 @@ export default { } return { - [isPg(ctx) ? Op.notILike : Op.notLike]: `%${value}%`, + [isPg(ctx) ? Op.notILike : Op.notLike]: `%${escapeLike(value)}%`, }; }, $startsWith(value, ctx) { if (Array.isArray(value)) { const conditions = value.map((item) => ({ - [isPg(ctx) ? Op.iLike : Op.like]: `${item}%`, + [isPg(ctx) ? Op.iLike : Op.like]: `${escapeLike(item)}%`, })); return { @@ -46,14 +50,14 @@ export default { } return { - [isPg(ctx) ? Op.iLike : Op.like]: `${value}%`, + [isPg(ctx) ? Op.iLike : Op.like]: `${escapeLike(value)}%`, }; }, $notStartsWith(value, ctx) { if (Array.isArray(value)) { const conditions = value.map((item) => ({ - [isPg(ctx) ? Op.notILike : Op.notLike]: `${item}%`, + [isPg(ctx) ? Op.notILike : Op.notLike]: `${escapeLike(item)}%`, })); return { @@ -62,14 +66,14 @@ export default { } return { - [isPg(ctx) ? Op.notILike : Op.notLike]: `${value}%`, + [isPg(ctx) ? Op.notILike : Op.notLike]: `${escapeLike(value)}%`, }; }, $endWith(value, ctx) { if (Array.isArray(value)) { const conditions = value.map((item) => ({ - [isPg(ctx) ? Op.iLike : Op.like]: `%${item}`, + [isPg(ctx) ? Op.iLike : Op.like]: `%${escapeLike(item)}`, })); return { @@ -78,14 +82,14 @@ export default { } return { - [isPg(ctx) ? Op.iLike : Op.like]: `%${value}`, + [isPg(ctx) ? Op.iLike : Op.like]: `%${escapeLike(value)}`, }; }, $notEndWith(value, ctx) { if (Array.isArray(value)) { const conditions = value.map((item) => ({ - [isPg(ctx) ? Op.notILike : Op.notLike]: `%${item}`, + [isPg(ctx) ? Op.notILike : Op.notLike]: `%${escapeLike(item)}`, })); return { @@ -94,7 +98,7 @@ export default { } return { - [isPg(ctx) ? Op.notILike : Op.notLike]: `%${value}`, + [isPg(ctx) ? Op.notILike : Op.notLike]: `%${escapeLike(value)}`, }; }, } as Record;