Skip to content

Commit 60f7e9e

Browse files
committed
feat: add sql.escape function
1 parent 799bccb commit 60f7e9e

File tree

3 files changed

+86
-1
lines changed

3 files changed

+86
-1
lines changed

README.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Requires `sequelize@^4.0.0`. Once v5 is released I'll check if it's still
2323
compatible. Not making any effort to support versions < 4, but you're welcome
2424
to make a PR.
2525

26-
## Example
26+
## Examples
2727

2828
```js
2929
const Sequelize = require('sequelize')
@@ -44,3 +44,40 @@ WHERE ${User.attributes.birthday} = ${new Date('2346-7-11')} AND
4444
${Sequelize.literal(lock ? 'FOR UPDATE' : '')}`).then(console.log);
4545
// => [ [ { name: 'Jimbob' } ], Statement { sql: 'SELECT "name" FROM "Users" WHERE "birthday" = $1 AND "active" = $2 FOR UPDATE' } ]
4646
```
47+
48+
Sometimes custom subqueries within a Sequelize `where` clause can be useful.
49+
In this case, there is no way to use query parameters. You can use
50+
`sql.escape` in this context to inline the escaped values rather than using
51+
query parameters:
52+
53+
```js
54+
const {Op} = Sequelize
55+
56+
const User = sequelize.define('User', {
57+
name: {type: Sequelize.STRING},
58+
})
59+
const Organization = sequelize.define('Organization', {
60+
name: {type: Sequelize.STRING},
61+
})
62+
const OrganizationMember = sequelize.define('OrganizationMember', {
63+
userId: {type: Sequelize.INTEGER},
64+
organizationId: {type: Sequelize.INTEGER},
65+
})
66+
User.belongsToMany(Organization, {through: OrganizationMember})
67+
Organization.belongsToMany(User, {through: OrganizationMember})
68+
69+
async function getUsersInOrganization(organizationId, where = {}) {
70+
return await User.findAll({
71+
where: {
72+
...where,
73+
// Using a sequelize include clause to do this kind of sucks tbh
74+
id: {[Op.in]: Sequelize.literal(sql.escape`
75+
SELECT ${OrganizationMember.attributes.userId}
76+
FROM ${OrganizationMember}
77+
WHERE ${OrganizationMember.attributes.organizationId} = ${organizationId}
78+
`)}
79+
// SELECT "userId" FROM "OrganizationMembers" WHERE "organizationId" = 2
80+
},
81+
})
82+
}
83+
```

src/index.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,32 @@ function sql(
3030
return {bind, query: parts.join('').trim().replace(/\s+/g, ' ')}
3131
}
3232

33+
sql.escape = function escapeSql(
34+
strings: $ReadOnlyArray<string>,
35+
...expressions: $ReadOnlyArray<mixed>
36+
): string {
37+
const parts: Array<string> = []
38+
let queryGenerator
39+
for (let i = 0, length = expressions.length; i < length; i++) {
40+
parts.push(strings[i])
41+
const expression = expressions[i]
42+
if (expression instanceof Literal) {
43+
parts.push(expression.val)
44+
} else if (expression && expression.prototype instanceof Model) {
45+
const {QueryGenerator, tableName} = (expression: any)
46+
queryGenerator = QueryGenerator
47+
parts.push(QueryGenerator.quoteTable(tableName))
48+
} else if (expression && expression.type instanceof Sequelize.ABSTRACT) {
49+
const {field, Model: {QueryGenerator}} = (expression: any)
50+
queryGenerator = QueryGenerator
51+
parts.push(QueryGenerator.quoteIdentifier(field))
52+
} else {
53+
if (!queryGenerator) throw new Error(`at least one of the expressions must be a sequelize Model or attribute`)
54+
parts.push(queryGenerator.escape(expression))
55+
}
56+
}
57+
parts.push(strings[expressions.length])
58+
return parts.join('').trim().replace(/\s+/g, ' ')
59+
}
60+
3361
module.exports = sql

test/index.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,23 @@ WHERE ${User.attributes.birthday} = ${new Date('2346-7-11')} AND
2323
})
2424
})
2525
})
26+
27+
describe(`sql.escape`, function () {
28+
it(`works`, function () {
29+
const sequelize = new Sequelize('test', 'test', 'test', {dialect: 'postgres'})
30+
31+
const User = sequelize.define('User', {
32+
name: {type: Sequelize.STRING},
33+
birthday: {type: Sequelize.DATE},
34+
})
35+
36+
expect(sql.escape`
37+
SELECT ${User.attributes.id} ${Sequelize.literal('FROM')} ${User}
38+
WHERE ${User.attributes.name} LIKE ${'and%'} AND
39+
${User.attributes.id} = ${1}
40+
`).to.deep.equal(`SELECT "id" FROM "Users" WHERE "name" LIKE 'and%' AND "id" = 1`)
41+
})
42+
it(`throws if it can't get a QueryGenerator`, function () {
43+
expect(() => sql.escape`SELECT ${1} + ${2};`).to.throw(Error, 'at least one of the expressions must be a sequelize Model or attribute')
44+
})
45+
})

0 commit comments

Comments
 (0)