Permalink
Browse files

feat: finish mysql adapter makeFieldWhere

  • Loading branch information...
XadillaX committed Aug 12, 2016
1 parent e7b8c56 commit eb7076e9ea09a66c94a37806afc23707cd3d877e
Showing with 372 additions and 14 deletions.
  1. +292 −0 lib/adapters/mysql.js
  2. +0 −1 lib/field_type/json.js
  3. +38 −13 lib/model.js
  4. +42 −0 util/escaper.js
@@ -6,10 +6,27 @@
*/
"use strict";

const _ = require("lodash");
const cu = require("config.util");
const debug = require("debug")("toshihiko:adapter:mysql");

const Adapter = require("./base");
const escaper = require("../../util/escaper");

const FIELD_LOGICS = {
"$eq": "=",
"===": "=",
"$neq": "!=",
"!==": "!=",
"$lt": "<",
"<": "<",
"$gt": ">",
">": ">",
"$lte": "<=",
"$gte": ">=",
"$like": "LIKE",
"$in": "IN"
};

function onConnection() {
this.emit("log", "A new MySQL connection from Toshihiko is set. ⁽⁽ଘ( ˙꒳˙ )ଓ⁾⁾");
@@ -105,6 +122,281 @@ class MySQLAdapter extends Adapter {
debug("created.", this);
}

makeFieldWhere(model, key, condition, logic) {
/**
* CAUTION:
*
* if field type is NOT NEED QUOTES
*
* type author should escape SQL in `restore()` by themselves
*/

logic = logic.toUpperCase() === "OR" ? "OR" : "AND";
const field = model.fieldNamesMap[key];
if(!field) {
throw new Error(`no field named "${key}" in model "${model.name}"`);
}

// { foo: 1 }
//
// =>
//
// FOO = 1
if(typeof condition !== "object") {
condition = field.restore(condition);
let sql = `\`${field.column}\` = `;
if(field.type.needQuotes) {
sql += `"${escaper.escape(condition)}"`;
} else {
sql += condition;
}
return sql;
}

if(condition === null) {
return `\`${field.column}\` IS NULL`;
}

let redundant = false;
const fragments = [];
Object.keys(condition).forEach(fieldLogic => {
if(redundant) return;

fieldLogic = fieldLogic.toLowerCase();
let fragCond = condition[fieldLogic];
switch(fieldLogic) {
case "$and":
case "$or": {
// using array
// eg. { $or: [ a, b, c ] }
if(!Array.isArray(fragCond)) {
fragCond = [ fragCond ];
}
fieldLogic = fieldLogic === "$and" ? "AND" : "OR";
const temp = fragCond.map(value => this.makeFieldWhere(model, key, value, fieldLogic));
let sql = temp.join(` ${fieldLogic} `);
if(temp.length > 1) sql = `(${sql})`;
fragments.push(sql);
break;
}

case "$eq":
case "===":
case "$neq":
case "!==":
case "$lt":
case "<":
case "$gt":
case ">":
case "$lte":
case "<=":
case "$gte":
case ">=":
case "$like":
case "$in": {
const symbol = FIELD_LOGICS[fieldLogic];

// regard `IN` as special
if("IN" === symbol) {
let sql = `\`${field.column}\` IN `;
let seg = fragCond.map(value => field.restore(value));
if(field.type.needQuotes) {
seg = seg.map(value => `("${escaper.escape(value)}")`);
}
sql += `(${seg.join(", ")})`;
fragments.push(sql);
break;
}

let and = [];
let or = [];

if(fragCond !== null && typeof fragCond === "object" && (fragCond.$or || fragCond.$and)) {
// logic object
// eg. { $eq: { $or: [ 1, 2, 3 ] } }
if(fragCond.$and) {
and = fragCond.$and;
}

if(fragCond.$or) {
or = fragCond.$or;
}
} else {
// using array
// eg. { $neq: [ 1, 2, 3 ] }
if(!Array.isArray(fragCond)) {
fragCond = [ fragCond ];
}

and = fragCond;
}

const closure = value => {
if((symbol === "=" || symbol === "!=") && value === null) {
return `\`${field.column}\` IS ${symbol === "=" ? "NULL" : "NOT NULL"}`;
}

value = field.restore(value);
debug(`${field.column} =>`, value);

if(field.type.needQuotes) value = `("${escaper.escape(value)}")`;
debug(`${field.column} =>`, value);
return `\`${field.column}\` ${symbol} ${value}`;
};
const andSeg = and.map(closure);
const orSeg = or.map(closure);

let andSql = andSeg.join(" AND ");
if(andSeg.length > 1) andSql = `(${andSql})`;
let orSql = orSeg.join(" OR ");
if(orSeg.length > 1) orSql = `(${orSql})`;

let sql = (andSeg.length && orSeg.length) ?
`(${andSql} AND ${orSql})` :
(andSeg.length ?
andSql :
orSql);
fragments.push(sql);

break;
}

default: redundant = true; break;
}
});

// not redundant condition data with correct logic conditions
if(!redundant && fragments.length) {
let sql = fragments.join(` ${logic} `);
if(fragments.length > 1) sql = `(${sql})`;
return sql;
}

// if condition is an object but not something like
// $eq, $neq, $like, ...
//
// we regard it as something like { foo: new Date() }
condition = field.restore(condition);
debug(`${field.column} => ${condition}`);
let sql = `\`${field.column}\` = `;
if(field.type.needQuotes) {
sql += `"${escaper.escape(condition)}"`;
} else {
sql += condition;
}

return sql;
}

makeArrayWhere(model, condition, logic) {
logic = logic.toUpperCase() === "OR" ? "OR" : "AND";
return `(${condition.map(cond => this.makeWhere(model, cond, "AND")).join(` ${logic} `)})`;
}

makeWhere(model, condition, logic) {
logic = logic.toUpperCase() === "OR" ? "OR" : "AND";

if(Array.isArray(condition)) {
return this.makeArrayWhere(model, condition, logic);
}

// SQL fragment variable
const fragments = [];
Object.keys(condition).forEach(function(key) {
const fragCond = condition[key];
switch(key) {
/**
* + { $and: { foo: 1, bar: 2 } }
* + { $and: [ { foo: 1 }, { bar: 2 } ] }
*/
case "$and":
case "$or": {
if(!Array.isArray(fragCond)) {
fragments.push(this.makeWhere(model, fragCond, key.substr(1)));
} else {
fragments.push(this.makeArrayWhere(model, fragCond, key.substr(1)));
}
break;
}

/**
* { key1: 1, key2: { $neq: 2 } }
*/
default: fragments.push(this.makeFieldWhere(model, key, fragCond, logic)); break;
}
});

// piecing together fragments
//
// fragments: [ "`FOO` = 1", "(`BAR` = 2 OR `BAZ` = 3" ]
// logic: AND
//
// =>
//
// (`FOO` = 1 AND (`BAR` = 2 OR `BAZ` = 3))
return `(${fragments.join(` ${logic} `)})`;
}

makeFind(model, options) {
let fields = options.fields;
if(!fields || !fields.length) {
fields = null;
}

let sql = "SELECT ";
sql += options.count ?
"COUNT(0)" :
fields ?
_.compact(fields.map(field => `\`${model.nameToColumn[field]}\``)).join(", ") :
"*";
sql += ` FROM \`${model.name}\``;

if(options.where && Object.keys(options.where)) {
sql += ` WHERE ${this.makeWhere(model, options.where)}`;
}

return sql;
}

makeSql(type, model, options) {
switch(type) {
case "find": return this.makeFind(model, options);
default: return this.makeFind(model, options);
}
}

findWithNoCache(query, callback, options) {
const _options = cu.extendDeep({}, {
fields: query._fields,
where: query._where,
order: query._order,
limit: query._limit
}, options);

const sql = this.makeSql("find", query.model, _options);
this.execute(sql, function(err, rows) {
if(err) {
return callback(err, undefined, sql);
}

return callback(undefined, rows, sql);
});
}

find(query, callback, options) {
// if no cache detected
if(!query.cache) {
return this.findWithNoCache(query, callback, options);
}

// const _options = cu.extendDeep({}, {
// fields: query._fields,
// where: query._where,
// order: query._order,
// limit: query._limit
// }, options);
}

execute(sql, params, callback) {
if(typeof params === "function") {
callback = params;
@@ -62,4 +62,3 @@ Json.equal = function(a, b) {
Json.defaultValue = {};

module.exports = Json;

@@ -33,24 +33,25 @@ class ToshihikoModel extends EventEmitter {
originalSchema: {
value: schema
},
schema: {
enumerable: true,
value: schema.map(options => {
const field = new Field(options);
if(field.primaryKey) {
this.primaryKeys.push(field);
}

if(field.autoIncrement) this.ai = field;

return field;
})
},
options: {
value: options || {}
}
});

Object.defineProperty(this, "schema", {
enumerable: true,
value: schema.map(options => {
const field = new Field(options);
if(field.primaryKey) {
this.primaryKeys.push(field);
}

if(field.autoIncrement) this.ai = field;

return field;
})
});

if(!this.primaryKeys.length) {
this.emit("log", `!!! WARNING: YOU'D BETTER ADD PRIMARY KEY(S) IN MODEL ${this.name} !!!`);
}
@@ -77,6 +78,30 @@ class ToshihikoModel extends EventEmitter {
});
}

// some key maps
Object.defineProperties(this, {
nameToColumn: { value: {} },
columnToName: { value: {} },
fieldColumnsMap: { value: {} },
fieldNamesMap: { value: {} }
});
this.schema.forEach(field => {
this.nameToColumn[field.name] = field.column;
this.columnToName[field.column] = field.name;
this.fieldColumnsMap[field.column] = field;
this.fieldNamesMap[field.name] = field;
});

// be compatible with 0.x
Object.defineProperty(this, "_fieldsKeyMap", {
value: {
n2c: this.nameToColumn,
c2n: this.columnToName,
name: this.fieldNamesMap,
column: this.fieldColumnsMap
}
});

debug(`"${this.name}" created.`, this);
}

Oops, something went wrong.

0 comments on commit eb7076e

Please sign in to comment.