Skip to content

Commit

Permalink
feat(sequelize): adds integration with sequelize
Browse files Browse the repository at this point in the history
Relates to #8
  • Loading branch information
stalniy committed Aug 17, 2020
1 parent 2499b2f commit 1710361
Show file tree
Hide file tree
Showing 10 changed files with 955 additions and 68 deletions.
29 changes: 29 additions & 0 deletions packages/sql/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,35 @@ const condition = new CompoundCondition('and', [
const query = interpret(condition, User.query())
```

### [Sequelize](https://sequelize.org/)

```js
import { interpret } from '@ucast/sql/sequelize';
import { CompoundCondition, FieldCondition } from '@ucast/core';
import { Model, Sequelize, DataTypes } from 'sequelize';

const sequelize = new Sequelize('sqlite::memory:');

class User extends Model {}

User.init({
name: { type: DataTypes.STRING },
blocked: { type: DataTypes.BOOLEAN },
lastLoggedIn: { type: DataTypes.DATETIME },
});

const condition = new CompoundCondition('and', [
new FieldCondition('eq', 'blocked', false),
new FieldCondition('lt', 'lastLoggedIn', Date.now() - 24 * 3600 * 1000),
]);

// {
// include: [],
// where: literal('(`blocked` = 0 and lastLoggedIn < 1597594415354)')
// }
const query = interpret(condition, User)
```

## Want to help?

Want to file a bug, contribute some code, or improve documentation? Excellent! Read up on guidelines for [contributing]
Expand Down
13 changes: 11 additions & 2 deletions packages/sql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
"typings": "dist/types/index.d.ts",
"scripts": {
"build.types": "tsc",
"build": "npm run build.sql && npm run build.objection",
"build": "npm run build.sql && npm run build.objection && npm run build.sequelize",
"prebuild": "rm -rf dist/* && npm run build.types",
"build.sql": "BUILD_TYPES=es6m,es6c rollup -c ../../rollup.config.js -e @ucast/core",
"build.objection": "npm run build.sql -- -i src/lib/objection.ts -e objection -e ../index",
"build.sequelize": "npm run build.sql -- -i src/lib/sequelize.ts -e sequelize -e ../index",
"lint": "eslint --ext .js,.ts src/ spec/",
"test": "mocha -r ts-node/register spec/*",
"coverage": "nyc -n src npm run test && nyc report --reporter=lcov",
Expand Down Expand Up @@ -55,9 +56,11 @@
"@semantic-release/git": "^9.0.0",
"@semantic-release/github": "^7.0.7",
"@semantic-release/npm": "^7.0.5",
"@types/bluebird": "^3.5.32",
"@types/chai": "^4.2.11",
"@types/chai-spies": "^1.0.1",
"@types/mocha": "^7.0.2",
"@types/validator": "^13.1.0",
"@typescript-eslint/eslint-plugin": "^3.6.0",
"chai": "^4.2.0",
"chai-spies": "^1.0.0",
Expand All @@ -72,15 +75,21 @@
"rollup": "^2.15.0",
"rollup-plugin-terser": "^6.1.0",
"semantic-release": "^17.1.1",
"sequelize": "6.3.4",
"sqlite3": "^5.0.0",
"ts-node": "^8.10.2",
"typescript": "^3.9.5"
},
"peerDependencies": {
"objection": "^2.0.0"
"objection": "^2.0.0",
"sequelize": "^5.0.0 || ^6.0.0"
},
"peerDependenciesMeta": {
"objection": {
"optional": true
},
"sequelize": {
"optional": true
}
},
"dependencies": {
Expand Down
7 changes: 7 additions & 0 deletions packages/sql/sequelize/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "@ucast/sql-sequelize",
"private": true,
"typings": "../dist/types/lib/sequelize.d.ts",
"main": "../dist/es6c/lib/sequelize.js",
"es2015": "../dist/es6m/lib/sequelize.js"
}
4 changes: 2 additions & 2 deletions packages/sql/spec/objection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { interpret } from '../src/lib/objection'
import { expect, linearize } from './specHelper'

describe('Condition interpreter for Objection', () => {
const { User } = configureObjection()
const { User } = configureORM()

it('returns `QueryBuilder`', () => {
const condition = new FieldCondition('eq', 'name', 'test')
Expand Down Expand Up @@ -45,7 +45,7 @@ describe('Condition interpreter for Objection', () => {
})
})

function configureObjection() {
function configureORM() {
Model.knex(Knex({ client: 'pg' }))

class User extends Model {
Expand Down
56 changes: 56 additions & 0 deletions packages/sql/spec/sequelize.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { FieldCondition } from '@ucast/core'
import { Model, Sequelize, DataTypes } from 'sequelize'
import { interpret } from '../src/lib/sequelize'
import { expect } from './specHelper'

describe('Condition interpreter for Sequelize', () => {
const { User } = configureORM()

it('returns an object with `where` and `include` keys', () => {
const condition = new FieldCondition('eq', 'name', 'test')
const query = interpret(condition, User)

expect(query).to.be.an('object')
expect(query.where.val).to.equal('`name` = \'test\'')
expect(query.include).to.be.an('array').that.is.empty
})

it('properly binds parameters for "IN" operator', () => {
const condition = new FieldCondition('in', 'age', [1, 2, 3])
const query = interpret(condition, User)

expect(query.where.val).to.equal('`age` in(1, 2, 3)')
})

it('automatically inner joins relation when condition is set on relation field', () => {
const condition = new FieldCondition('eq', 'projects.name', 'test')
const query = interpret(condition, User)

expect(query.include).to.deep.equal([
{ association: 'projects', required: true }
])
expect(query.where.val).to.equal('`projects`.`name` = \'test\'')
})
})

function configureORM() {
const sequelize = new Sequelize('sqlite::memory:')

class User extends Model {}
class Project extends Model {}

User.init({
name: { type: DataTypes.STRING },
email: { type: DataTypes.STRING },
}, { sequelize, modelName: 'user' })

Project.init({
name: { type: DataTypes.STRING },
active: { type: DataTypes.BOOLEAN }
}, { sequelize, modelName: 'project' })

Project.belongsTo(User)
User.hasMany(Project)

return { User, Project }
}
55 changes: 55 additions & 0 deletions packages/sql/src/dialects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,58 @@ export const mssql = {
paramPlaceholder: questionPlaceholder,
escapeField: (field: string) => `[${field}]`,
};

export interface DialectOptions {
regexp(field: string, placeholder: string, ignoreCase: boolean): string
joinRelation?(relationName: string, context: unknown): boolean
escapeField(field: string): string
paramPlaceholder(index: number): string
}

export type SupportedDialects = 'mssql' |
'postgres' |
'pg' |
'oracle' |
'oracledb' |
'mysql' |
'mysql2' |
'mariadb' |
'sqlite3' |
'sqlite';
type Dialects<V> = Record<SupportedDialects, DialectOptions & V>;

export function createDialects<T extends Partial<DialectOptions>>(options: T): Dialects<T> {
const mssqlOptions = {
...mssql,
...options,
};
const pgOptions = {
...pg,
...options,
};
const oracleOptions = {
...oracle,
...options,
};
const mysqlOptions = {
...mysql,
...options,
};
const sqliteOptions = {
...sqlite,
...options,
};

return {
mssql: mssqlOptions,
oracle: oracleOptions,
oracledb: oracleOptions,
pg: pgOptions,
postgres: pgOptions,
mysql: mysqlOptions,
mysql2: mysqlOptions,
mariadb: mysqlOptions,
sqlite: sqliteOptions,
sqlite3: sqliteOptions,
};
}
7 changes: 2 additions & 5 deletions packages/sql/src/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@ import {
Condition,
InterpretationContext
} from '@ucast/core';
import { DialectOptions } from './dialects';

export interface SqlQueryOptions {
export interface SqlQueryOptions extends Required<DialectOptions> {
rootAlias?: string
regexp(field: string, placeholder: string, ignoreCase: boolean): string
joinRelation(relationName: string, context: unknown): boolean
escapeField(field: string): string
paramPlaceholder(index: number): string
}

export class Query {
Expand Down
42 changes: 5 additions & 37 deletions packages/sql/src/lib/objection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@ import {
createSqlInterpreter,
allInterpreters,
SqlOperator,
sqlite,
mssql,
pg,
oracle,
mysql
createDialects,
mysql,
} from '../index';

function joinRelation(relationName: string, query: QueryBuilder<Model>) {
Expand All @@ -20,38 +17,9 @@ function joinRelation(relationName: string, query: QueryBuilder<Model>) {
return true;
}

const dialects = {
mssql: {
...mssql,
joinRelation,
paramPlaceholder: mysql.paramPlaceholder,
},
postgres: {
...pg,
joinRelation,
paramPlaceholder: mysql.paramPlaceholder,
},
oracle: {
...oracle,
joinRelation,
paramPlaceholder: mysql.paramPlaceholder,
},
mysql: {
...mysql,
joinRelation,
},
sqlite: {
...sqlite,
joinRelation,
paramPlaceholder: mysql.paramPlaceholder,
},
};

Object.assign(dialects, {
mysql2: dialects.mysql,
oracledb: dialects.oracle,
sqlite3: dialects.sqlite,
pg: dialects.postgres,
const dialects = createDialects({
joinRelation,
paramPlaceholder: mysql.paramPlaceholder,
});

export function createInterpreter(interpreters: Record<string, SqlOperator<any>>) {
Expand Down
39 changes: 39 additions & 0 deletions packages/sql/src/lib/sequelize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Condition } from '@ucast/core';
import { ModelType, Utils, literal } from 'sequelize';
import {
createSqlInterpreter,
allInterpreters,
SqlOperator,
createDialects,
mysql
} from '../index';

function joinRelation(relationName: string, Model: ModelType) {
return Model.associations.hasOwnProperty(relationName);
}

const dialects = createDialects({
joinRelation,
paramPlaceholder: mysql.paramPlaceholder,
});

export function createInterpreter(interpreters: Record<string, SqlOperator<any>>) {
const interpretSQL = createSqlInterpreter(interpreters);

return (condition: Condition, Model: ModelType) => {
const dialect = Model.sequelize!.getDialect() as keyof typeof dialects;
const options = dialects[dialect];

if (!options) {
throw new Error(`Unsupported database dialect: ${dialect}`);
}

const [sql, params, joins] = interpretSQL(condition, options, Model);
return {
include: joins.map(association => ({ association, required: true })),
where: literal(Utils.format([sql, ...(params as string[])], dialect)),
};
};
}

export const interpret = createInterpreter(allInterpreters);
Loading

0 comments on commit 1710361

Please sign in to comment.