@wener/miniquery
Version:
SQL Where like **safe** filter expression for ORM.
157 lines (142 loc) • 4.43 kB
text/typescript
import {
DataTypes,
Sequelize,
type FindOptions,
type Includeable,
type ModelStatic,
type WhereOptions,
} from '@sequelize/core';
import { beforeAll, expect, test, type TestContext } from 'vitest';
import { toMiniQueryAST } from '../ast';
import { toSequelizeWhere } from './where';
let sequelize: Sequelize;
beforeAll(async () => {
return;
sequelize = new Sequelize('sqlite::memory:');
const Profile = await sequelize.define(
'Profile',
{ name: { type: DataTypes.STRING }, attributes: { type: DataTypes.JSON } },
{ underscored: true },
);
const User = await sequelize.define(
'User',
{
username: { type: DataTypes.STRING },
name: { type: DataTypes.STRING },
age: { type: DataTypes.INTEGER },
attributes: { type: DataTypes.JSON },
profileId: { type: DataTypes.INTEGER, references: { model: Profile, key: 'id' } },
avatarUrl: { type: DataTypes.STRING },
},
{ underscored: true },
);
const Department = await sequelize.define(
'Department',
{ fullName: { type: DataTypes.STRING }, attributes: { type: DataTypes.JSON } },
{ underscored: true },
);
const Pet = await sequelize.define(
'Pet',
{ fullName: { type: DataTypes.STRING }, attributes: { type: DataTypes.JSON } },
{ underscored: true },
);
const Image = await sequelize.define(
'Image',
{ imageUrl: { type: DataTypes.STRING }, attributes: { type: DataTypes.JSON } },
{ underscored: true },
);
Department.hasMany(User, { as: 'users' });
User.belongsTo(Department, { as: 'department' });
User.hasMany(Pet, { as: 'pets' });
Pet.belongsTo(User, { as: 'user' });
User.hasOne(Profile, { as: 'profile' });
Profile.belongsTo(User, { as: 'user' });
Profile.hasOne(Image, { as: 'avatar' });
Image.belongsTo(Profile);
await sequelize.sync();
});
// fixme 无法处理关联
test.skip('sequelize where', async (t) => {
const { User } = sequelize.models;
for (const s of [
'',
`age > -1`,
`User.age > -1`,
// `length(profile.name) > 1`,
`length(name) > length(username)`,
`name = 'wener' and age > 18 and age < 80`,
`name = 'wener' and (age > 18 or age < 80)`,
`age not between 18 and 80`,
`date(createdAt) between '2020-01-01' and '2020-01-31' and length(name) > 1 and username is not null and name like 'wen%'`,
// `attributes.user.name = 'wener'`, // json
// `attributes.'user name' = 'wener'`, // json
// `attributes.'user.name' = 'wener'`, // 无法正确 escape
`age > 0 and age > 0 and age > 0`, // should flatten
`avatarUrl is not null`, // handle case
]) {
await assertQuery(t, User, s);
}
});
test.skip('sequelize association', async (t) => {
const { User } = sequelize.models;
for (const s of [`profile.name is not null`, `profile.avatar.imageUrl is not null`]) {
await assertQuery(t, User, s);
}
});
async function assertQuery(_t: TestContext, Model: ModelStatic<any>, query: string) {
let where: WhereOptions;
let include: Includeable[];
try {
({ where, include } = toSequelizeWhere(query, { sequelize, Model }));
} catch (e) {
console.log(`MiniQuery: ${query}`);
console.log(`AST`, toMiniQueryAST(query));
throw e;
}
let sql = '';
try {
await Model.findAll({
logging: (s) => {
const indexOf = s.indexOf('WHERE ');
if (indexOf < 0) {
return (sql = '');
}
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
return (sql = s.substring(indexOf + 'WHERE '.length, s.length - 1));
},
where,
include,
});
} catch (e: any) {
console.error('ERROR', e?.message);
console.log(`SQL: ${sql}`);
console.log(`MiniQuery: ${query}`);
console.log(`Where`, where);
throw e;
}
expect(sql, `Query: ${query}`).matchSnapshot();
}
test.skip('sequelize incorrect', async () => {
const { User } = sequelize.models;
for (const s of [
'1 > 0',
`name.not_ok = ''`,
`not_exists = ''`,
`avatar_url is not null`, // attr not found
`pets.fullName is not null`, // use has instead
`pets.fullName: 'wener'`, // not supported yet
]) {
let opt: FindOptions = {};
expect(() => {
opt = toSequelizeWhere(s, { sequelize, Model: User });
console.log(`Where`, opt.where);
}, `Query: ${s}`).throw();
await User.findAll({ ...opt });
}
});
test.skip('sequelize type', async () => {
const { type } = sequelize.models.User.getAttributes().attributes as any;
expect(type).toBeInstanceOf(DataTypes.JSON);
// sqlite do not support json
expect(String(type)).toBe('TEXT');
});