projection翻译时对无用metadata的过滤

This commit is contained in:
Xu Chang 2022-07-11 18:19:01 +08:00
parent b7c64e67c9
commit b9b9145705
16 changed files with 2684 additions and 86 deletions

119
lib/MySQL/connector.js Normal file
View File

@ -0,0 +1,119 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MySqlConnector = void 0;
const mysql2_1 = __importDefault(require("mysql2"));
const uuid_1 = require("uuid");
const assert_1 = __importDefault(require("assert"));
class MySqlConnector {
pool;
configuration;
txnDict;
constructor(configuration) {
this.configuration = configuration;
this.txnDict = {};
}
connect() {
this.pool = mysql2_1.default.createPool(this.configuration);
}
disconnect() {
this.pool.end();
}
startTransaction(option) {
return new Promise((resolve, reject) => {
this.pool.getConnection((err, connection) => {
if (err) {
return reject(err);
}
const { isolationLevel } = option || {};
const startTxn = () => {
let sql = 'START TRANSACTION;';
connection.query(sql, (err2) => {
if (err2) {
connection.release();
return reject(err2);
}
const id = (0, uuid_1.v4)();
Object.assign(this.txnDict, {
[id]: connection,
});
resolve(id);
});
};
if (isolationLevel) {
connection.query(`SET TRANSACTION ISOLATION LEVEL ${isolationLevel};`, (err2) => {
if (err2) {
connection.release();
return reject(err2);
}
startTxn();
});
}
else {
startTxn();
}
});
});
}
async exec(sql, txn) {
if (process.env.NODE_ENV === 'development') {
console.log(sql);
}
if (txn) {
const connection = this.txnDict[txn];
(0, assert_1.default)(connection);
return new Promise((resolve, reject) => {
connection.query(sql, (err, result) => {
if (err) {
console.error(`sql exec err: ${sql}`, err);
return reject(err);
}
resolve(result);
});
});
}
else {
return new Promise((resolve, reject) => {
// if (process.env.DEBUG) {
// console.log(sql);
//}
this.pool.query(sql, (err, result) => {
if (err) {
console.error(`sql exec err: ${sql}`, err);
return reject(err);
}
resolve(result);
});
});
}
}
commitTransaction(txn) {
const connection = this.txnDict[txn];
(0, assert_1.default)(connection);
return new Promise((resolve, reject) => {
connection.query('COMMIT;', (err) => {
if (err) {
return reject(err);
}
connection.release();
resolve();
});
});
}
rollbackTransaction(txn) {
const connection = this.txnDict[txn];
(0, assert_1.default)(connection);
return new Promise((resolve, reject) => {
connection.query('ROLLBACK;', (err) => {
if (err) {
return reject(err);
}
connection.release();
resolve();
});
});
}
}
exports.MySqlConnector = MySqlConnector;

246
lib/MySQL/store.js Normal file
View File

@ -0,0 +1,246 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MysqlStore = void 0;
const CascadeStore_1 = require("oak-domain/lib/store/CascadeStore");
const connector_1 = require("./connector");
const translator_1 = require("./translator");
const lodash_1 = require("lodash");
const assert_1 = __importDefault(require("assert"));
const relation_1 = require("oak-domain/lib/store/relation");
function convertGeoTextToObject(geoText) {
if (geoText.startsWith('POINT')) {
const coord = geoText.match((/(\d|\.)+(?=\)|\s)/g));
return {
type: 'Point',
coordinates: coord.map(ele => parseFloat(ele)),
};
}
else {
throw new Error('only support Point now');
}
}
class MysqlStore extends CascadeStore_1.CascadeStore {
connector;
translator;
constructor(storageSchema, configuration) {
super(storageSchema);
this.connector = new connector_1.MySqlConnector(configuration);
this.translator = new translator_1.MySqlTranslator(storageSchema);
}
supportManyToOneJoin() {
return true;
}
supportMultipleCreate() {
return true;
}
formResult(entity, result) {
const schema = this.getSchema();
function resolveAttribute(entity2, r, attr, value) {
const { attributes, view } = schema[entity2];
if (!view) {
const i = attr.indexOf(".");
if (i !== -1) {
const attrHead = attr.slice(0, i);
const attrTail = attr.slice(i + 1);
if (!r[attrHead]) {
r[attrHead] = {};
}
const rel = (0, relation_1.judgeRelation)(schema, entity2, attrHead);
(0, assert_1.default)(rel === 2 || typeof rel === 'string');
resolveAttribute(typeof rel === 'string' ? rel : attrHead, r[attrHead], attrTail, value);
}
else if (attributes[attr]) {
const { type } = attributes[attr];
switch (type) {
case 'date':
case 'time': {
if (value instanceof Date) {
r[attr] = value.valueOf();
}
else {
r[attr] = value;
}
break;
}
case 'geometry': {
if (typeof value === 'string') {
r[attr] = convertGeoTextToObject(value);
}
else {
r[attr] = value;
}
break;
}
case 'object':
case 'array': {
if (typeof value === 'string') {
r[attr] = JSON.parse(value.replace(/[\r]/g, '\\r').replace(/[\n]/g, '\\n'));
}
else {
r[attr] = value;
}
break;
}
case 'function': {
if (typeof value === 'string') {
// 函数的执行环境需要的参数只有创建函数者知悉只能由上层再创建Function
r[attr] = `return ${Buffer.from(value, 'base64').toString()}`;
}
else {
r[attr] = value;
}
break;
}
case 'bool':
case 'boolean': {
if (value === 0) {
r[attr] = false;
}
else if (value === 1) {
r[attr] = true;
}
else {
r[attr] = value;
}
break;
}
default: {
r[attr] = value;
}
}
}
else {
r[attr] = value;
}
}
else {
(0, lodash_1.assign)(r, {
[attr]: value,
});
}
}
function formalizeNullObject(r, e) {
const { attributes: a2 } = schema[e];
let allowFormalize = true;
for (let attr in r) {
if (typeof r[attr] === 'object' && a2[attr] && a2[attr].type === 'ref') {
if (formalizeNullObject(r[attr], a2[attr].ref)) {
r[attr] = null;
}
else {
allowFormalize = false;
}
}
else if (r[attr] !== null) {
allowFormalize = false;
}
}
return allowFormalize;
}
function formSingleRow(r) {
let result2 = {};
for (let attr in r) {
const value = r[attr];
resolveAttribute(entity, result2, attr, value);
}
formalizeNullObject(result2, entity);
return result2;
}
if (result instanceof Array) {
return result.map(r => formSingleRow(r));
}
return formSingleRow(result);
}
async selectAbjointRow(entity, selection, context, params) {
const sql = this.translator.translateSelect(entity, selection, params);
const result = await this.connector.exec(sql, context.getCurrentTxnId());
return this.formResult(entity, result);
}
async updateAbjointRow(entity, operation, context, params) {
const { translator, connector } = this;
const { action } = operation;
const txn = context.getCurrentTxnId();
switch (action) {
case 'create': {
const { data } = operation;
const sql = translator.translateInsert(entity, data instanceof Array ? data : [data]);
await connector.exec(sql, txn);
context.opRecords.push({
a: 'c',
d: data,
e: entity,
});
return data instanceof Array ? data.length : 1;
}
case 'remove': {
const sql = translator.translateRemove(entity, operation, params);
await connector.exec(sql, txn);
// todo 这里对sorter和indexfrom/count的支持不完整
context.opRecords.push({
a: 'r',
e: entity,
f: operation.filter,
});
return 1;
}
default: {
(0, assert_1.default)(!['select', 'download', 'stat'].includes(action));
const sql = translator.translateUpdate(entity, operation, params);
await connector.exec(sql, txn);
// todo 这里对sorter和indexfrom/count的支持不完整
context.opRecords.push({
a: 'u',
e: entity,
d: operation.data,
f: operation.filter,
});
return 1;
}
}
}
async operate(entity, operation, context, params) {
const { action } = operation;
(0, assert_1.default)(!['select', 'download', 'stat'].includes(action), '现在不支持使用select operation');
return await this.cascadeUpdate(entity, operation, context, params);
}
async select(entity, selection, context, params) {
const result = await this.cascadeSelect(entity, selection, context, params);
return {
result,
};
}
async count(entity, selection, context, params) {
const sql = this.translator.translateCount(entity, selection, params);
const result = await this.connector.exec(sql, context.getCurrentTxnId());
return result.count;
}
async begin(option) {
const txn = await this.connector.startTransaction(option);
return txn;
}
async commit(txnId) {
await this.connector.commitTransaction(txnId);
}
async rollback(txnId) {
await this.connector.rollbackTransaction(txnId);
}
connect() {
this.connector.connect();
}
disconnect() {
this.connector.disconnect();
}
async initialize(dropIfExists) {
const schema = this.getSchema();
for (const entity in schema) {
const sqls = this.translator.translateCreateEntity(entity, { replace: dropIfExists });
for (const sql of sqls) {
await this.connector.exec(sql);
}
}
}
}
exports.MysqlStore = MysqlStore;

708
lib/MySQL/translator.js Normal file
View File

@ -0,0 +1,708 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MySqlTranslator = void 0;
const assert_1 = __importDefault(require("assert"));
const util_1 = require("util");
const lodash_1 = require("lodash");
const luxon_1 = require("luxon");
const sqlTranslator_1 = require("../sqlTranslator");
const GeoTypes = [
{
type: 'point',
name: "Point"
},
{
type: 'path',
name: "LineString",
element: 'point',
},
{
name: "MultiLineString",
element: "path",
multiple: true,
},
{
type: 'polygon',
name: "Polygon",
element: "path"
},
{
name: "MultiPoint",
element: "point",
multiple: true,
},
{
name: "MultiPolygon",
element: "polygon",
multiple: true,
}
];
function transformGeoData(data) {
if (data instanceof Array) {
const element = data[0];
if (element instanceof Array) {
return ` GeometryCollection(${data.map(ele => transformGeoData(ele)).join(',')})`;
}
else {
const geoType = GeoTypes.find(ele => ele.type === element.type);
if (!geoType) {
throw new Error(`${element.type} is not supported in MySQL`);
}
const multiGeoType = GeoTypes.find(ele => ele.element === geoType.type && ele.multiple);
return ` ${multiGeoType.name}(${data.map(ele => transformGeoData(ele)).join(',')})`;
}
}
else {
const { type, coordinate } = data;
const geoType = GeoTypes.find(ele => ele.type === type);
if (!geoType) {
throw new Error(`${data.type} is not supported in MySQL`);
}
const { element, name } = geoType;
if (!element) {
// Point
return ` ${name}(${coordinate.join(',')})`;
}
// Polygon or Linestring
return ` ${name}(${coordinate.map((ele) => transformGeoData({
type: element,
coordinate: ele,
}))})`;
}
}
class MySqlTranslator extends sqlTranslator_1.SqlTranslator {
getDefaultSelectFilter(alias, hint) {
if (hint?.includeDeleted) {
return '';
}
return ` \`${alias}\`.\`$$deleteAt$$\` is null`;
}
modifySchema() {
for (const entity in this.schema) {
const { attributes, indexes } = this.schema[entity];
const geoIndexes = [];
for (const attr in attributes) {
if (attributes[attr].type === 'geometry') {
const geoIndex = indexes?.find((idx) => idx.config?.type === 'spatial' && idx.attributes.find((attrDef) => attrDef.name === attr));
if (!geoIndex) {
geoIndexes.push({
name: `${entity}_geo_${attr}`,
attributes: [{
name: attr,
}],
config: {
type: 'spatial',
}
});
}
}
}
if (geoIndexes.length > 0) {
if (indexes) {
indexes.push(...geoIndexes);
}
else {
(0, lodash_1.assign)(this.schema[entity], {
indexes: geoIndexes,
});
}
}
}
}
constructor(schema) {
super(schema);
// MySQL为geometry属性默认创建索引
this.modifySchema();
}
static supportedDataTypes = [
// numeric types
"bit",
"int",
"integer",
"tinyint",
"smallint",
"mediumint",
"bigint",
"float",
"double",
"double precision",
"real",
"decimal",
"dec",
"numeric",
"fixed",
"bool",
"boolean",
// date and time types
"date",
"datetime",
"timestamp",
"time",
"year",
// string types
"char",
"nchar",
"national char",
"varchar",
"nvarchar",
"national varchar",
"blob",
"text",
"tinyblob",
"tinytext",
"mediumblob",
"mediumtext",
"longblob",
"longtext",
"enum",
"set",
"binary",
"varbinary",
// json data type
"json",
// spatial data types
"geometry",
"point",
"linestring",
"polygon",
"multipoint",
"multilinestring",
"multipolygon",
"geometrycollection"
];
static spatialTypes = [
"geometry",
"point",
"linestring",
"polygon",
"multipoint",
"multilinestring",
"multipolygon",
"geometrycollection"
];
static withLengthDataTypes = [
"char",
"varchar",
"nvarchar",
"binary",
"varbinary"
];
static withPrecisionDataTypes = [
"decimal",
"dec",
"numeric",
"fixed",
"float",
"double",
"double precision",
"real",
"time",
"datetime",
"timestamp"
];
static withScaleDataTypes = [
"decimal",
"dec",
"numeric",
"fixed",
"float",
"double",
"double precision",
"real"
];
static unsignedAndZerofillTypes = [
"int",
"integer",
"smallint",
"tinyint",
"mediumint",
"bigint",
"decimal",
"dec",
"numeric",
"fixed",
"float",
"double",
"double precision",
"real"
];
static withWidthDataTypes = [
'int',
];
static dataTypeDefaults = {
"varchar": { length: 255 },
"nvarchar": { length: 255 },
"national varchar": { length: 255 },
"char": { length: 1 },
"binary": { length: 1 },
"varbinary": { length: 255 },
"decimal": { precision: 10, scale: 0 },
"dec": { precision: 10, scale: 0 },
"numeric": { precision: 10, scale: 0 },
"fixed": { precision: 10, scale: 0 },
"float": { precision: 12 },
"double": { precision: 22 },
"time": { precision: 0 },
"datetime": { precision: 0 },
"timestamp": { precision: 0 },
"bit": { width: 1 },
"int": { width: 11 },
"integer": { width: 11 },
"tinyint": { width: 4 },
"smallint": { width: 6 },
"mediumint": { width: 9 },
"bigint": { width: 20 }
};
maxAliasLength = 63;
populateDataTypeDef(type, params) {
if (MySqlTranslator.withLengthDataTypes.includes(type)) {
if (params) {
const { length } = params;
return `${type}(${length}) `;
}
else {
const { length } = MySqlTranslator.dataTypeDefaults[type];
return `${type}(${length}) `;
}
}
if (MySqlTranslator.withPrecisionDataTypes.includes(type)) {
if (params) {
const { precision, scale } = params;
if (typeof scale === 'number') {
return `${type}(${precision}, ${scale}) `;
}
return `${type}(${precision})`;
}
else {
const { precision, scale } = MySqlTranslator.dataTypeDefaults[type];
if (typeof scale === 'number') {
return `${type}(${precision}, ${scale}) `;
}
return `${type}(${precision})`;
}
}
if (MySqlTranslator.withWidthDataTypes.includes(type)) {
(0, assert_1.default)(type === 'int');
const { width } = params;
switch (width) {
case 1: {
return 'tinyint';
}
case 2: {
return 'smallint';
}
case 3: {
return 'mediumint';
}
case 4: {
return 'int';
}
default: {
return 'bigint';
}
}
}
if (['date'].includes(type)) {
return 'datetime';
}
if (['object', 'array'].includes(type)) {
return 'text ';
}
if (['image', 'function'].includes(type)) {
return 'text ';
}
if (type === 'ref') {
return 'char(36)';
}
return `${type} `;
}
translateAttrProjection(dataType, alias, attr) {
switch (dataType) {
case 'geometry': {
return ` st_astext(\`${alias}\`.\`${attr}\`)`;
}
default: {
return ` \`${alias}\`.\`${attr}\``;
}
}
}
translateAttrValue(dataType, value) {
if (value === null) {
return 'null';
}
switch (dataType) {
case 'geometry': {
return transformGeoData(value);
}
case 'date': {
if (value instanceof Date) {
return luxon_1.DateTime.fromJSDate(value).toFormat('yyyy-LL-dd HH:mm:ss');
}
else if (typeof value === 'number') {
return luxon_1.DateTime.fromMillis(value).toFormat('yyyy-LL-dd HH:mm:ss');
}
return value;
}
case 'object':
case 'array': {
return this.escapeStringValue(JSON.stringify(value));
}
/* case 'function': {
return `'${Buffer.from(value.toString()).toString('base64')}'`;
} */
default: {
if (typeof value === 'string') {
return this.escapeStringValue(value);
}
return value;
}
}
}
translateFullTextSearch(value, entity, alias) {
const { $search } = value;
const { indexes } = this.schema[entity];
const ftIndex = indexes && indexes.find((ele) => {
const { config } = ele;
return config && config.type === 'fulltext';
});
(0, assert_1.default)(ftIndex);
const { attributes } = ftIndex;
const columns2 = attributes.map(({ name }) => `${alias}.${name}`);
return ` match(${columns2.join(',')}) against ('${$search}' in natural language mode)`;
}
translateCreateEntity(entity, options) {
const replace = options?.replace;
const { schema } = this;
const entityDef = schema[entity];
const { storageName, attributes, indexes, view } = entityDef;
// todo view暂还不支持
const entityType = view ? 'view' : 'table';
let sql = `create ${entityType} `;
if (storageName) {
sql += `\`${storageName}\` `;
}
else {
sql += `\`${entity}\` `;
}
if (view) {
throw new Error(' view unsupported yet');
}
else {
sql += '(';
// 翻译所有的属性
Object.keys(attributes).forEach((attr, idx) => {
const attrDef = attributes[attr];
const { type, params, default: defaultValue, unique, notNull, } = attrDef;
sql += `\`${attr}\` `;
sql += this.populateDataTypeDef(type, params);
if (notNull || type === 'geometry') {
sql += ' not null ';
}
if (unique) {
sql += ' unique ';
}
if (defaultValue !== undefined) {
(0, assert_1.default)(type !== 'ref');
sql += ` default ${this.translateAttrValue(type, defaultValue)}`;
}
if (attr === 'id') {
sql += ' primary key';
}
if (idx < Object.keys(attributes).length - 1) {
sql += ',\n';
}
});
// 翻译索引信息
if (indexes) {
sql += ',\n';
indexes.forEach(({ name, attributes, config }, idx) => {
const { unique, type, parser } = config || {};
if (unique) {
sql += ' unique ';
}
else if (type === 'fulltext') {
sql += ' fulltext ';
}
else if (type === 'spatial') {
sql += ' spatial ';
}
sql += `index ${name} `;
if (type === 'hash') {
sql += ` using hash `;
}
sql += '(';
let includeDeleteAt = false;
attributes.forEach(({ name, size, direction }, idx2) => {
sql += `\`${name}\``;
if (size) {
sql += ` (${size})`;
}
if (direction) {
sql += ` ${direction}`;
}
if (idx2 < attributes.length - 1) {
sql += ',';
}
if (name === '$$deleteAt$$') {
includeDeleteAt = true;
}
});
if (!includeDeleteAt && !type) {
sql += ', $$deleteAt$$';
}
sql += ')';
if (parser) {
sql += ` with parser ${parser}`;
}
if (idx < indexes.length - 1) {
sql += ',\n';
}
});
}
}
sql += ')';
if (!replace) {
return [sql];
}
return [`drop ${entityType} \`${storageName || entity}\`;`, sql];
}
translateFnName(fnName, argumentNumber) {
switch (fnName) {
case '$add': {
return '%s + %s';
}
case '$subtract': {
return '%s - %s';
}
case '$multiply': {
return '%s * %s';
}
case '$divide': {
return '%s / %s';
}
case '$abs': {
return 'ABS(%s)';
}
case '$round': {
return 'ROUND(%s, %s)';
}
case '$ceil': {
return 'CEIL(%s)';
}
case '$floor': {
return 'FLOOR(%s)';
}
case '$pow': {
return 'POW(%s, %s)';
}
case '$gt': {
return '%s > %s';
}
case '$gte': {
return '%s >= %s';
}
case '$lt': {
return '%s < %s';
}
case '$lte': {
return '%s <= %s';
}
case '$eq': {
return '%s = %s';
}
case '$ne': {
return '%s <> %s';
}
case '$startsWith': {
return '%s like CONCAT(%s, \'%\')';
}
case '$endsWith': {
return '%s like CONCAT(\'%\', %s)';
}
case '$includes': {
return '%s like CONCAT(\'%\', %s, \'%\')';
}
case '$true': {
return '%s = true';
}
case '$false': {
return '%s = false';
}
case '$and': {
let result = '';
for (let iter = 0; iter < argumentNumber; iter++) {
result += '%s';
if (iter < argumentNumber - 1) {
result += ' and ';
}
}
return result;
}
case '$or': {
let result = '';
for (let iter = 0; iter < argumentNumber; iter++) {
result += '%s';
if (iter < argumentNumber - 1) {
result += ' or ';
}
}
return result;
}
case '$not': {
return 'not %s';
}
case '$year': {
return 'YEAR(%s)';
}
case '$month': {
return 'MONTH(%s)';
}
case '$weekday': {
return 'WEEKDAY(%s)';
}
case '$weekOfYear': {
return 'WEEKOFYEAR(%s)';
}
case '$day': {
return 'DAY(%s)';
}
case '$dayOfMonth': {
return 'DAYOFMONTH(%s)';
}
case '$dayOfWeek': {
return 'DAYOFWEEK(%s)';
}
case '$dayOfYear': {
return 'DAYOFYEAR(%s)';
}
case '$dateDiff': {
return 'DATEDIFF(%s, %s, %s)';
}
case '$contains': {
return 'ST_CONTAINS(%s, %s)';
}
case '$distance': {
return 'ST_DISTANCE(%s, %s)';
}
default: {
throw new Error(`unrecoganized function ${fnName}`);
}
}
}
translateExpression(alias, expression, refDict) {
const translateConstant = (constant) => {
if (typeof constant === 'string') {
return `'${constant}'`;
}
else if (constant instanceof Date) {
return `'${luxon_1.DateTime.fromJSDate(constant).toFormat('yyyy-LL-dd HH:mm:ss')}'`;
}
else {
(0, assert_1.default)(typeof constant === 'number');
return `${constant}`;
}
};
const translateInner = (expr) => {
const k = Object.keys(expr);
let result;
if (k.includes('#attr')) {
const attrText = `\`${alias}\`.\`${(expr)['#attr']}\``;
result = attrText;
}
else if (k.includes('#refId')) {
const refId = (expr)['#refId'];
const refAttr = (expr)['#refAttr'];
(0, assert_1.default)(refDict[refId]);
const attrText = `\`${refDict[refId]}\`.\`${refAttr}\``;
result = attrText;
}
else {
(0, assert_1.default)(k.length === 1);
if ((expr)[k[0]] instanceof Array) {
const fnName = this.translateFnName(k[0], (expr)[k[0]].length);
const args = [fnName];
args.push(...(expr)[k[0]].map((ele) => {
if (['string', 'number'].includes(typeof ele) || ele instanceof Date) {
return translateConstant(ele);
}
else {
return translateInner(ele);
}
}));
result = util_1.format.apply(null, args);
}
else {
const fnName = this.translateFnName(k[0], 1);
const args = [fnName];
const arg = (expr)[k[0]];
if (['string', 'number'].includes(typeof arg) || arg instanceof Date) {
args.push(translateConstant(arg));
}
else {
args.push(translateInner(arg));
}
result = util_1.format.apply(null, args);
}
}
return result;
};
return translateInner(expression);
}
populateSelectStmt(projectionText, fromText, selection, aliasDict, filterText, sorterText, indexFrom, count) {
const { hint } = selection;
// todo hint of use index
let sql = `select ${projectionText} from ${fromText}`;
if (filterText) {
sql += ` where ${filterText}`;
}
if (sorterText) {
sql += ` order by ${sorterText}`;
}
if (typeof indexFrom === 'number') {
(0, assert_1.default)(typeof count === 'number');
sql += ` limit ${indexFrom}, ${count}`;
}
if (hint?.mysql?.forUpdate) {
sql += ' for update';
}
return sql;
}
populateUpdateStmt(updateText, fromText, aliasDict, filterText, sorterText, indexFrom, count, params) {
// todo using index
const alias = aliasDict['./'];
const now = luxon_1.DateTime.now().toFormat('yyyy-LL-dd HH:mm:ss');
let sql = `update ${fromText} set ${updateText ? `${updateText},` : ''} \`${alias}\`.\`$$updateAt$$\` = '${now}'`;
if (filterText) {
sql += ` where ${filterText}`;
}
if (sorterText) {
sql += ` order by ${sorterText}`;
}
if (typeof indexFrom === 'number') {
(0, assert_1.default)(typeof count === 'number');
sql += ` limit ${indexFrom}, ${count}`;
}
return sql;
}
populateRemoveStmt(removeText, fromText, aliasDict, filterText, sorterText, indexFrom, count, params) {
// todo using index
const alias = aliasDict['./'];
const now = luxon_1.DateTime.now().toFormat('yyyy-LL-dd HH:mm:ss');
let sql = `update ${fromText} set \`${alias}\`.\`$$deleteAt$$\` = '${now}'`;
if (filterText) {
sql += ` where ${filterText}`;
}
if (sorterText) {
sql += ` order by ${sorterText}`;
}
if (typeof indexFrom === 'number') {
(0, assert_1.default)(typeof count === 'number');
sql += ` limit ${indexFrom}, ${count}`;
}
return sql;
}
}
exports.MySqlTranslator = MySqlTranslator;

View File

@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

17
lib/index.js Normal file
View File

@ -0,0 +1,17 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./MySQL/store"), exports);

719
lib/sqlTranslator.js Normal file
View File

@ -0,0 +1,719 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SqlTranslator = void 0;
const assert_1 = __importDefault(require("assert"));
const lodash_1 = require("lodash");
const luxon_1 = require("luxon");
const types_1 = require("oak-domain/lib/types");
const relation_1 = require("oak-domain/lib/store/relation");
class SqlTranslator {
schema;
constructor(schema) {
this.schema = this.makeFullSchema(schema);
}
makeFullSchema(schema2) {
const schema = (0, lodash_1.cloneDeep)(schema2);
for (const entity in schema) {
const { attributes, indexes } = schema[entity];
// 增加默认的属性
(0, lodash_1.assign)(attributes, {
id: {
type: 'char',
params: {
length: 36,
},
},
$$createAt$$: {
type: 'date',
notNull: true,
},
$$updateAt$$: {
type: 'date',
notNull: true,
},
$$deleteAt$$: {
type: 'date',
},
$$triggerData$$: {
type: 'object',
},
$$triggerTimestamp$$: {
type: 'date',
},
});
// 增加默认的索引
const intrinsticIndexes = [
{
name: `${entity}_create_at`,
attributes: [{
name: '$$createAt$$',
}]
}, {
name: `${entity}_update_at`,
attributes: [{
name: '$$updateAt$$',
}],
}, {
name: `${entity}_trigger_ts`,
attributes: [{
name: '$$triggerTimestamp$$',
}],
}
];
// 增加外键上的索引
for (const attr in attributes) {
if (attributes[attr].type === 'ref') {
if (!(indexes?.find(ele => ele.attributes[0].name === attr))) {
intrinsticIndexes.push({
name: `${entity}_fk_${attr}`,
attributes: [{
name: attr,
}]
});
}
}
if (attr === 'entity' && attributes[attr].type === 'varchar') {
const entityIdDef = attributes.entityId;
if (entityIdDef?.type === 'varchar') {
if (!(indexes?.find(ele => ele.attributes[0].name === 'entity' && ele.attributes[1]?.name === 'entityId'))) {
intrinsticIndexes.push({
name: `${entity}_fk_entity_entityId`,
attributes: [{
name: 'entity',
}, {
name: 'entityId',
}]
});
}
}
}
}
if (indexes) {
indexes.push(...intrinsticIndexes);
}
else {
(0, lodash_1.assign)(schema[entity], {
indexes: intrinsticIndexes,
});
}
}
return schema;
}
getStorageName(entity) {
const { storageName } = this.schema[entity];
return (storageName || entity);
}
translateInsert(entity, data) {
const { schema } = this;
const { attributes, storageName = entity } = schema[entity];
let sql = `insert into \`${storageName}\`(`;
const attrs = Object.keys(data[0]).filter(ele => attributes.hasOwnProperty(ele));
attrs.forEach((attr, idx) => {
sql += ` \`${attr}\``;
if (idx < attrs.length - 1) {
sql += ',';
}
});
sql += ', `$$createAt$$`, `$$updateAt$$`) values ';
const now = luxon_1.DateTime.now().toFormat('yyyy-LL-dd HH:mm:ss');
data.forEach((d, dataIndex) => {
sql += '(';
attrs.forEach((attr, attrIdx) => {
const attrDef = attributes[attr];
const { type: dataType } = attrDef;
const value = this.translateAttrValue(dataType, d[attr]);
sql += value;
if (attrIdx < attrs.length - 1) {
sql += ',';
}
});
sql += `, '${now}', '${now}')`;
if (dataIndex < data.length - 1) {
sql += ',';
}
});
return sql;
}
/**
* analyze the join relations in projection/query/sort
* 所有的层次关系都当成left join处理如果有内表为空的情况请手动处理
* {
* b: {
* name: {
* $exists: false,
* }
* }
* }
* 这样的query会把内表为空的行也返回
* @param param0
*/
analyzeJoin(entity, { projection, filter, sorter, isStat }, initialNumber) {
const { schema } = this;
let number = initialNumber || 1;
const projectionRefAlias = {};
const filterRefAlias = {};
let extraWhere = '';
const alias = `${entity}_${number++}`;
let from = ` \`${this.getStorageName(entity)}\` \`${alias}\` `;
const aliasDict = {
'./': alias,
};
const analyzeFilterNode = ({ node, path, entityName, alias }) => {
Object.keys(node).forEach((op) => {
if (['$and', '$or'].includes(op)) {
node[op].forEach((subNode) => analyzeFilterNode({
node: subNode,
path,
entityName,
alias,
}));
}
else {
const rel = (0, relation_1.judgeRelation)(this.schema, entityName, op);
if (typeof rel === 'string') {
let alias2;
const pathAttr = `${path}${op}/`;
if (!aliasDict.hasOwnProperty(pathAttr)) {
alias2 = `${rel}_${number++}`;
(0, lodash_1.assign)(aliasDict, {
[pathAttr]: alias2,
});
from += ` left join \`${this.getStorageName(rel)}\` \`${alias2}\` on \`${alias}\`.\`${op}Id\` = \`${alias2}\`.\`id\``;
}
else {
alias2 = aliasDict[pathAttr];
}
analyzeFilterNode({
node: node[op],
path: pathAttr,
entityName: rel,
alias: alias2,
});
}
else if (rel === 2) {
let alias2;
const pathAttr = `${path}${op}/`;
if (!aliasDict.hasOwnProperty(pathAttr)) {
alias2 = `${op}_${number++}`;
(0, lodash_1.assign)(aliasDict, {
[pathAttr]: alias2,
});
from += ` left join \`${this.getStorageName(op)}\` \`${alias2}\` on \`${alias}\`.\`entityId\` = \`${alias2}\`.\`id\``;
extraWhere += `\`${alias}\`.\`entity\` = '${op}'`;
}
else {
alias2 = aliasDict[pathAttr];
}
analyzeFilterNode({
node: node[op],
path: pathAttr,
entityName: rel,
alias: alias2,
});
}
else {
// 不支持一对多
(0, assert_1.default)(rel === 0 || rel === 1);
}
}
});
if (node['#id']) {
(0, assert_1.default)(!filterRefAlias[node['#id']]);
(0, lodash_1.assign)(filterRefAlias, {
[node['#id']]: alias,
});
}
};
if (filter) {
analyzeFilterNode({
node: filter,
path: './',
entityName: entity,
alias,
});
}
const analyzeSortNode = ({ node, path, entityName, alias }) => {
const attr = (0, lodash_1.keys)(node)[0];
const rel = (0, relation_1.judgeRelation)(this.schema, entityName, attr);
if (typeof rel === 'string') {
const pathAttr = `${path}${attr}/`;
let alias2;
if (!aliasDict.hasOwnProperty(pathAttr)) {
alias2 = `${rel}_${number++}`;
(0, lodash_1.assign)(aliasDict, {
[pathAttr]: alias2,
});
from += ` left join \`${this.getStorageName(rel)}\` \`${alias2}\` on \`${alias}\`.\`${attr}Id\` = \`${alias2}\`.\`id\``;
}
else {
alias2 = aliasDict[pathAttr];
}
analyzeSortNode({
node: node[attr],
path: pathAttr,
entityName: rel,
alias: alias2,
});
}
else if (rel === 2) {
const pathAttr = `${path}${attr}/`;
let alias2;
if (!aliasDict.hasOwnProperty(pathAttr)) {
alias2 = `${attr}_${number++}`;
(0, lodash_1.assign)(aliasDict, {
[pathAttr]: alias2,
});
from += ` left join \`${this.getStorageName(attr)}\` \`${alias2}\` on \`${alias}\`.\`entityId\` = \`${alias2}\`.\`id\``;
extraWhere += `\`${alias}\`.\`entity\` = '${attr}'`;
}
else {
alias2 = aliasDict[pathAttr];
}
analyzeSortNode({
node: node[attr],
path: pathAttr,
entityName: attr,
alias: alias2,
});
}
else {
(0, assert_1.default)(rel === 0 || rel === 1);
}
};
if (sorter) {
sorter.forEach((sortNode) => {
analyzeSortNode({
node: sortNode.$attr,
path: './',
entityName: entity,
alias,
});
});
}
const analyzeProjectionNode = ({ node, path, entityName, alias }) => {
const { attributes } = schema[entityName];
Object.keys(node).forEach((attr) => {
const rel = (0, relation_1.judgeRelation)(this.schema, entityName, attr);
if (typeof rel === 'string') {
const pathAttr = `${path}${attr}/`;
let alias2;
if (!aliasDict.hasOwnProperty(pathAttr)) {
alias2 = `${rel}_${number++}`;
(0, lodash_1.assign)(aliasDict, {
[pathAttr]: alias2,
});
from += ` left join \`${this.getStorageName(rel)}\` \`${alias2}\` on \`${alias}\`.\`${attr}Id\` = \`${alias2}\`.\`id\``;
}
else {
alias2 = aliasDict[pathAttr];
}
analyzeProjectionNode({
node: node[attr],
path: pathAttr,
entityName: rel,
alias: alias2,
});
}
else if (rel === 2) {
const pathAttr = `${path}${attr}/`;
let alias2;
if (!aliasDict.hasOwnProperty(pathAttr)) {
alias2 = `${attr}_${number++}`;
(0, lodash_1.assign)(aliasDict, {
[pathAttr]: alias2,
});
from += ` left join \`${this.getStorageName(attr)}\` \`${alias2}\` on \`${alias}\`.\`entityId\` = \`${alias2}\`.\`id\``;
}
else {
alias2 = aliasDict[pathAttr];
}
analyzeProjectionNode({
node: node[attr],
path: pathAttr,
entityName: attr,
alias: alias2,
});
extraWhere += `\`${alias}\`.\`entity\` = '${attr}'`;
}
});
if (node['#id']) {
(0, assert_1.default)(!projectionRefAlias[node['#id']]);
(0, lodash_1.assign)(projectionRefAlias, {
[node['#id']]: alias,
});
}
};
if (projection) {
analyzeProjectionNode({ node: projection, path: './', entityName: entity, alias });
}
return {
aliasDict,
from,
projectionRefAlias,
filterRefAlias,
extraWhere,
currentNumber: number,
};
}
translateComparison(attr, value, type) {
const SQL_OP = {
$gt: '>',
$lt: '<',
$gte: '>=',
$lte: '<=',
$eq: '=',
$ne: '<>',
};
if (Object.keys(SQL_OP).includes(attr)) {
return ` ${SQL_OP[attr]} ${this.translateAttrValue(type, value)}`;
}
switch (attr) {
case '$startsWith': {
return ` like '${value}%'`;
}
case '$endsWith': {
return ` like '%${value}'`;
}
case '$includes': {
return ` like '%${value}%'`;
}
default: {
throw new Error(`unrecoganized comparison operator ${attr}`);
}
}
}
translateElement(attr, value) {
(0, assert_1.default)(attr === '$exists'); // only support one operator now
if (value) {
return ' is not null';
}
return ' is null';
}
translateEvaluation(attr, value, entity, alias, type, initialNumber, refAlias) {
switch (attr) {
case '$text': {
// fulltext search
return {
stmt: this.translateFullTextSearch(value, entity, alias),
currentNumber: initialNumber,
};
}
case '$in':
case '$nin': {
const IN_OP = {
$in: 'in',
$nin: 'not in',
};
if (value instanceof Array) {
const values = value.map((v) => {
if (['varchar', 'char', 'text', 'nvarchar', 'ref'].includes(type)) {
return `'${v}'`;
}
else {
return `${v}`;
}
});
if (values.length > 0) {
return {
stmt: ` ${IN_OP[attr]}(${values.join(',')})`,
currentNumber: initialNumber,
};
}
else {
if (attr === '$in') {
return {
stmt: ' in (null)',
currentNumber: initialNumber,
};
}
else {
return {
stmt: ' is not null',
currentNumber: initialNumber,
};
}
}
}
else {
// sub query
const { stmt: subQueryStmt, currentNumber } = this.translateSelectInner(value.entity, value, initialNumber, refAlias, undefined);
return {
stmt: ` ${IN_OP[attr]}(${subQueryStmt})`,
currentNumber,
};
}
}
default: {
(0, assert_1.default)('$between' === attr);
const values = value.map((v) => {
if (['varchar', 'char', 'text', 'nvarchar', 'ref'].includes(type)) {
return `'${v}'`;
}
else {
return `${v}`;
}
});
return {
stmt: ` between ${values[0]} and ${values[1]}`,
currentNumber: initialNumber,
};
}
}
}
translateFilter(entity, selection, aliasDict, filterRefAlias, initialNumber, extraWhere) {
const { schema } = this;
const { filter, hint } = selection;
let currentNumber = initialNumber;
const translateInner = (entity2, path, filter2, type) => {
const alias = aliasDict[path];
const { attributes } = schema[entity2];
let whereText = type ? '' : this.getDefaultSelectFilter(alias, hint);
if (filter2) {
const attrs = Object.keys(filter2).filter(ele => !ele.startsWith('#'));
attrs.forEach((attr) => {
if (whereText) {
whereText += ' and ';
}
whereText + '(';
if (['$and', '$or', '$xor', '$not'].includes(attr)) {
let result = '';
switch (attr) {
case '$and':
case '$or':
case '$xor': {
const logicQueries = filter2[attr];
logicQueries.forEach((logicQuery, index) => {
const sql = translateInner(entity2, path, logicQuery);
if (sql) {
whereText += ` (${sql})`;
if (index < logicQueries.length - 1) {
whereText += ` ${attr.slice(1)}`;
}
}
});
break;
}
default: {
(0, assert_1.default)(attr === '$not');
const logicQuery = filter2[attr];
const sql = translateInner(entity2, path, logicQuery);
if (sql) {
whereText += ` not (${translateInner(entity2, path, logicQuery)})`;
break;
}
}
}
}
else if (attr.toLowerCase().startsWith(types_1.EXPRESSION_PREFIX)) {
// expression
whereText += ` (${this.translateExpression(alias, filter2[attr], filterRefAlias)})`;
}
else if (['$gt', '$gte', '$lt', '$lte', '$eq', '$ne', '$startsWith', '$endsWith', '$includes'].includes(attr)) {
whereText += this.translateComparison(attr, filter2[attr], type);
}
else if (['$exists'].includes(attr)) {
whereText += this.translateElement(attr, filter2[attr]);
}
else if (['$text', '$in', '$nin', '$between'].includes(attr)) {
const { stmt, currentNumber: cn2 } = this.translateEvaluation(attr, filter2[attr], entity2, alias, type, initialNumber, filterRefAlias);
whereText += stmt;
currentNumber = cn2;
}
else {
const rel = (0, relation_1.judgeRelation)(this.schema, entity, attr);
if (rel === 2) {
whereText += ` ${translateInner(attr, `${path}${attr}/`, filter2[attr])}`;
}
else if (typeof rel === 'string') {
whereText += ` ${translateInner(rel, `${path}${attr}/`, filter2[attr])}`;
}
else {
(0, assert_1.default)(attributes.hasOwnProperty(attr), `非法的属性${attr}`);
const { type: type2 } = attributes[attr];
// assert (type2 !== 'ref');
if (typeof filter2[attr] === 'object' && Object.keys(filter2[attr])[0] && Object.keys(filter2[attr])[0].startsWith('$')) {
whereText += ` \`${alias}\`.\`${attr}\` ${translateInner(entity2, path, filter2[attr], type2)}`;
}
else {
whereText += ` \`${alias}\`.\`${attr}\` = ${this.translateAttrValue(type2, filter2[attr])}`;
}
}
}
whereText + ')';
});
}
if (!whereText) {
whereText = 'true'; // 如果为空就赋一个永真条件以便处理and
}
return whereText;
};
const where = translateInner(entity, './', filter);
if (extraWhere && where) {
return {
stmt: `${extraWhere} and ${where}`,
currentNumber,
};
}
return {
stmt: extraWhere || where,
currentNumber,
};
}
translateSorter(entity, sorter, aliasDict) {
const translateInner = (entity2, sortAttr, path) => {
(0, assert_1.default)(Object.keys(sortAttr).length === 1);
const attr = Object.keys(sortAttr)[0];
const alias = aliasDict[path];
if (attr.toLocaleLowerCase().startsWith(types_1.EXPRESSION_PREFIX)) {
return this.translateExpression(alias, sortAttr[attr], {});
}
else if (sortAttr[attr] === 1) {
return `\`${alias}\`.\`${attr}\``;
}
else {
const rel = (0, relation_1.judgeRelation)(this.schema, entity2, attr);
if (typeof rel === 'string') {
return translateInner(rel, sortAttr[attr], `${path}${attr}/`);
}
else {
(0, assert_1.default)(rel === 2);
return translateInner(attr, sortAttr[attr], `${path}${attr}/`);
}
}
};
let sortText = '';
sorter.forEach((sortNode, index) => {
const { $attr, $direction } = sortNode;
sortText += translateInner(entity, $attr, './');
if ($direction) {
sortText += ` ${$direction}`;
}
if (index < sorter.length - 1) {
sortText += ',';
}
});
return sortText;
}
translateProjection(entity, projection, aliasDict, projectionRefAlias) {
const { schema } = this;
const translateInner = (entity2, projection2, path) => {
const alias = aliasDict[path];
const { attributes } = schema[entity2];
let projText = '';
let prefix = path.slice(2).replace(/\//g, '.');
const attrs = Object.keys(projection2).filter((attr) => {
if (attr.toLowerCase().startsWith(types_1.EXPRESSION_PREFIX)) {
return true;
}
const rel = (0, relation_1.judgeRelation)(this.schema, entity2, attr);
return [1, 2].includes(rel) || typeof rel === 'string';
});
attrs.forEach((attr, idx) => {
if (attr.toLowerCase().startsWith(types_1.EXPRESSION_PREFIX)) {
const exprText = this.translateExpression(alias, projection2[attr], projectionRefAlias);
projText += ` ${exprText} as ${prefix}${attr}`;
}
else {
const rel = (0, relation_1.judgeRelation)(this.schema, entity2, attr);
if (typeof rel === 'string') {
projText += translateInner(rel, projection2[attr], `${path}${attr}/`);
}
else if (rel === 2) {
projText += translateInner(attr, projection2[attr], `${path}${attr}/`);
}
else if (rel === 1) {
const { type } = attributes[attr];
if (projection2[attr] === 1) {
projText += ` ${this.translateAttrProjection(type, alias, attr)} as \`${prefix}${attr}\``;
}
else {
(0, assert_1.default)(typeof projection2 === 'string');
projText += ` ${this.translateAttrProjection(type, alias, attr)} as \`${prefix}${projection2[attr]}\``;
}
}
}
if (idx < attrs.length - 1) {
projText += ',';
}
});
return projText;
};
return translateInner(entity, projection, './');
}
translateSelectInner(entity, selection, initialNumber, refAlias, params) {
const { data, filter, sorter, indexFrom, count } = selection;
const { from: fromText, aliasDict, projectionRefAlias, extraWhere, filterRefAlias, currentNumber } = this.analyzeJoin(entity, {
projection: data,
filter,
sorter,
}, initialNumber);
(0, assert_1.default)((0, lodash_1.intersection)((0, lodash_1.keys)(refAlias), (0, lodash_1.keys)(filterRefAlias)).length === 0, 'filter中的#node结点定义有重复');
(0, lodash_1.assign)(refAlias, filterRefAlias);
const projText = this.translateProjection(entity, data, aliasDict, projectionRefAlias);
const { stmt: filterText, currentNumber: currentNumber2 } = this.translateFilter(entity, selection, aliasDict, refAlias, currentNumber, extraWhere);
const sorterText = sorter && this.translateSorter(entity, sorter, aliasDict);
return {
stmt: this.populateSelectStmt(projText, fromText, selection, aliasDict, filterText, sorterText, indexFrom, count),
currentNumber: currentNumber2,
};
}
translateSelect(entity, selection, params) {
const { stmt } = this.translateSelectInner(entity, selection, 1, {}, params);
return stmt;
}
translateCount(entity, selection, params) {
const { filter } = selection;
const { from: fromText, aliasDict, extraWhere, filterRefAlias, currentNumber } = this.analyzeJoin(entity, {
filter,
});
const projText = 'count(1)';
const { stmt: filterText } = this.translateFilter(entity, selection, aliasDict, filterRefAlias, currentNumber, extraWhere);
return this.populateSelectStmt(projText, fromText, selection, aliasDict, filterText, undefined, undefined, undefined);
}
translateRemove(entity, operation, params) {
const { filter, sorter, indexFrom, count } = operation;
const { aliasDict, filterRefAlias, extraWhere, from: fromText, currentNumber } = this.analyzeJoin(entity, { filter, sorter });
const alias = aliasDict['./'];
const { stmt: filterText } = this.translateFilter(entity, operation, aliasDict, filterRefAlias, currentNumber, extraWhere);
const sorterText = sorter && sorter.length > 0 ? this.translateSorter(entity, sorter, aliasDict) : undefined;
return this.populateRemoveStmt(alias, fromText, aliasDict, filterText, sorterText, indexFrom, count, params);
}
translateUpdate(entity, operation, params) {
const { attributes } = this.schema[entity];
const { filter, sorter, indexFrom, count, data } = operation;
const { aliasDict, filterRefAlias, extraWhere, from: fromText, currentNumber } = this.analyzeJoin(entity, { filter, sorter });
const alias = aliasDict['./'];
let updateText = '';
for (const attr in data) {
if (updateText) {
updateText += ',';
}
(0, assert_1.default)(attributes.hasOwnProperty(attr) && attributes[attr].type !== 'ref');
const value = this.translateAttrValue(attributes[attr].type, data[attr]);
updateText += `\`${alias}\`.\`${attr}\` = ${value}`;
}
const { stmt: filterText } = this.translateFilter(entity, operation, aliasDict, filterRefAlias, currentNumber, extraWhere);
const sorterText = sorter && this.translateSorter(entity, sorter, aliasDict);
return this.populateUpdateStmt(updateText, fromText, aliasDict, filterText, sorterText, indexFrom, count, params);
}
translateDestroyEntity(entity, truncate) {
const { schema } = this;
const { storageName = entity, view } = schema[entity];
let sql;
if (view) {
sql = `drop view if exists \`${storageName}\``;
}
else {
sql = truncate ? `truncate table \`${storageName}\`` : `drop table if exists \`${storageName}\``;
}
return sql;
}
escapeStringValue(value) {
const result = `'${value.replace(/'/g, '\\\'')}'`;
return result;
}
}
exports.SqlTranslator = SqlTranslator;

1
lib/types/Translator.js Normal file
View File

@ -0,0 +1 @@
"use strict";

View File

@ -5,7 +5,8 @@
"main": "src/index.ts",
"scripts": {
"test": "mocha",
"make:test:domain": "ts-node script/makeTestDomain.ts"
"make:test:domain": "ts-node script/makeTestDomain.ts",
"build": "tsc"
},
"dependencies": {
"lodash": "^4.17.21",

View File

@ -64,6 +64,9 @@ export class MySqlConnector {
}
async exec(sql: string, txn?: string): Promise<any> {
if (process.env.NODE_ENV === 'development') {
console.log(sql);
}
if (txn) {
const connection = this.txnDict[txn];
assert(connection);

View File

@ -5,6 +5,7 @@ import { MySqlConnector } from './connector';
import { MySqlTranslator, MySqlSelectParams } from './translator';
import { assign } from 'lodash';
import assert from 'assert';
import { judgeRelation } from 'oak-domain/lib/store/relation';
function convertGeoTextToObject(geoText: string): object {
@ -49,8 +50,9 @@ export class MysqlStore<ED extends EntityDict, Cxt extends Context<ED>> extends
if (!r[attrHead]) {
r[attrHead] = {};
}
assert(attributes[attrHead] && attributes[attrHead].type === 'ref');
resolveAttribute(attributes[attrHead].ref as string, r[attrHead], attrTail, value);
const rel = judgeRelation(schema, entity2, attrHead);
assert(rel === 2 || typeof rel === 'string');
resolveAttribute(typeof rel === 'string' ? rel : attrHead, r[attrHead], attrTail, value);
}
else if (attributes[attr]) {
const { type } = attributes[attr];
@ -223,7 +225,7 @@ export class MysqlStore<ED extends EntityDict, Cxt extends Context<ED>> extends
}
}
}
async operate<T extends keyof ED>(entity: T, operation: ED[T]['Operation'], context: Cxt, params?: OperateParams & MySqlSelectParams): Promise<OperationResult<ED>> {
async operate<T extends keyof ED>(entity: T, operation: ED[T]['Operation'], context: Cxt, params?: OperateParams): Promise<OperationResult<ED>> {
const { action } = operation;
assert(!['select', 'download', 'stat'].includes(action), '现在不支持使用select operation');
return await this.cascadeUpdate(entity, operation as any, context, params);
@ -234,7 +236,7 @@ export class MysqlStore<ED extends EntityDict, Cxt extends Context<ED>> extends
result,
};
}
async count<T extends keyof ED>(entity: T, selection: Omit<ED[T]['Selection'], 'data' | 'sorter' | 'action'>, context: Cxt, params?: Object): Promise<number> {
async count<T extends keyof ED>(entity: T, selection: Pick<ED[T]['Selection'], 'filter'>, context: Cxt, params?: Object): Promise<number> {
const sql = this.translator.translateCount(entity, selection, params);
const result = await this.connector.exec(sql, context.getCurrentTxnId());

View File

@ -93,10 +93,15 @@ type IndexHint = {
}
export interface MySqlSelectParams extends SelectParams {
indexHint?: IndexHint;
}
export class MySqlTranslator<ED extends EntityDict> extends SqlTranslator<ED> {
protected getDefaultSelectFilter<T extends keyof ED>(alias: string, hint: ED[T]['Selection']['hint']): string {
if (hint?.includeDeleted) {
return '';
}
return ` \`${alias}\`.\`$$deleteAt$$\` is null`;
}
private modifySchema() {
for (const entity in this.schema) {
const { attributes, indexes } = this.schema[entity];
@ -366,7 +371,7 @@ export class MySqlTranslator<ED extends EntityDict> extends SqlTranslator<ED> {
}
}
protected translateAttrValue(dataType: DataType, value: any): string {
protected translateAttrValue(dataType: DataType | Ref, value: any): string {
if (value === null) {
return 'null';
}
@ -722,8 +727,17 @@ export class MySqlTranslator<ED extends EntityDict> extends SqlTranslator<ED> {
return translateInner(expression);
}
protected populateSelectStmt(projectionText: string, fromText: string, aliasDict: Record<string, string>, filterText: string, sorterText?: string, indexFrom?: number, count?: number, params?: MySqlSelectParams): string {
// todo using index
protected populateSelectStmt<T extends keyof ED>(
projectionText: string,
fromText: string,
selection: ED[T]['Selection'],
aliasDict: Record<string, string>,
filterText: string,
sorterText?: string,
indexFrom?: number,
count?: number): string {
const { hint } = selection;
// todo hint of use index
let sql = `select ${projectionText} from ${fromText}`;
if (filterText) {
sql += ` where ${filterText}`;
@ -735,10 +749,9 @@ export class MySqlTranslator<ED extends EntityDict> extends SqlTranslator<ED> {
assert (typeof count === 'number');
sql += ` limit ${indexFrom}, ${count}`;
}
if (params?.forUpdate) {
if (hint?.mysql?.forUpdate) {
sql += ' for update';
}
sql += ';';
return sql;
}
@ -746,7 +759,7 @@ export class MySqlTranslator<ED extends EntityDict> extends SqlTranslator<ED> {
// todo using index
const alias = aliasDict['./'];
const now = DateTime.now().toFormat('yyyy-LL-dd HH:mm:ss');
let sql = `update ${fromText} set ${updateText}, \`${alias}\`.\`$$updateAt$$\` = '${now}'`;
let sql = `update ${fromText} set ${updateText ? `${updateText},` : ''} \`${alias}\`.\`$$updateAt$$\` = '${now}'`;
if (filterText) {
sql += ` where ${filterText}`;
}
@ -757,7 +770,6 @@ export class MySqlTranslator<ED extends EntityDict> extends SqlTranslator<ED> {
assert (typeof count === 'number');
sql += ` limit ${indexFrom}, ${count}`;
}
sql += ';';
return sql;
}
@ -776,7 +788,6 @@ export class MySqlTranslator<ED extends EntityDict> extends SqlTranslator<ED> {
assert (typeof count === 'number');
sql += ` limit ${indexFrom}, ${count}`;
}
sql += ';';
return sql;
}

View File

@ -1 +0,0 @@
export interface DbContext

View File

@ -0,0 +1 @@
export * from './MySQL/store';

View File

@ -1,12 +1,11 @@
import assert from 'assert';
import { assign, cloneDeep, keys, set } from 'lodash';
import { assign, cloneDeep, intersection, keys, set } from 'lodash';
import { DateTime } from 'luxon';
import { Attribute, DeduceCreateOperationData, DeduceSorterAttr, DeduceSorterItem, EntityDict, Expression, EXPRESSION_PREFIX, Index, Q_FullTextValue, RefOrExpression, StorageSchema } from "oak-domain/lib/types";
import { Attribute, DeduceCreateOperationData, DeduceSorterAttr, DeduceSorterItem, EntityDict, Expression, EXPRESSION_PREFIX, Index, Q_FullTextValue, Ref, RefOrExpression, StorageSchema } from "oak-domain/lib/types";
import { DataType } from "oak-domain/lib/types/schema/DataTypes";
import { judgeRelation } from 'oak-domain/lib/store/relation';
export type SelectParams = {
forUpdate?: boolean;
};
export abstract class SqlTranslator<ED extends EntityDict> {
@ -113,24 +112,25 @@ export abstract class SqlTranslator<ED extends EntityDict> {
return schema;
}
protected abstract getDefaultSelectFilter<T extends keyof ED>(alias: string, hint: ED[T]['Selection']['hint']): string;
protected abstract translateAttrProjection(dataType: DataType, alias: string, attr: string): string;
protected abstract translateAttrValue(dataType: DataType, value: any): string;
protected abstract translateAttrValue(dataType: DataType | Ref, value: any): string;
protected abstract translateFullTextSearch<T extends keyof ED>(value: Q_FullTextValue, entity: T, alias: string): string;
abstract translateCreateEntity<T extends keyof ED>(entity: T, option: { replace?: boolean }): string[];
protected abstract populateSelectStmt(
protected abstract populateSelectStmt<T extends keyof ED>(
projectionText: string,
fromText: string,
selection: ED[T]['Selection'],
aliasDict: Record<string, string>,
filterText: string,
sorterText?: string,
indexFrom?: number,
count?: number,
params?: SelectParams): string;
count?: number): string;
protected abstract populateUpdateStmt(
updateText: string,
@ -222,20 +222,21 @@ export abstract class SqlTranslator<ED extends EntityDict> {
filter?: ED[T]['Selection']['filter'];
sorter?: ED[T]['Selection']['sorter'];
isStat?: true;
}): {
}, initialNumber?: number): {
aliasDict: Record<string, string>;
projectionRefAlias: Record<string, string>;
filterRefAlias: Record<string, string>;
from: string;
extraWhere: string;
currentNumber: number;
} {
const { schema } = this;
let count = 1;
let number = initialNumber || 1;
const projectionRefAlias: Record<string, string> = {};
const filterRefAlias: Record<string, string> = {};
let extraWhere = '';
const alias = `${entity as string}_${count++}`;
const alias = `${entity as string}_${number++}`;
let from = ` \`${this.getStorageName(entity)}\` \`${alias}\` `;
const aliasDict: Record<string, string> = {
'./': alias,
@ -265,7 +266,7 @@ export abstract class SqlTranslator<ED extends EntityDict> {
let alias2: string;
const pathAttr = `${path}${op}/`;
if (!aliasDict.hasOwnProperty(pathAttr)) {
alias2 = `${rel}_${count++}`;
alias2 = `${rel}_${number++}`;
assign(aliasDict, {
[pathAttr]: alias2,
});
@ -285,7 +286,7 @@ export abstract class SqlTranslator<ED extends EntityDict> {
let alias2: string;
const pathAttr = `${path}${op}/`;
if (!aliasDict.hasOwnProperty(pathAttr)) {
alias2 = `${op}_${count++}`;
alias2 = `${op}_${number++}`;
assign(aliasDict, {
[pathAttr]: alias2,
});
@ -338,7 +339,7 @@ export abstract class SqlTranslator<ED extends EntityDict> {
const pathAttr = `${path}${attr}/`;
let alias2: string;
if (!aliasDict.hasOwnProperty(pathAttr)) {
alias2 = `${rel}_${count++}`;
alias2 = `${rel}_${number++}`;
assign(aliasDict, {
[pathAttr]: alias2,
});
@ -358,7 +359,7 @@ export abstract class SqlTranslator<ED extends EntityDict> {
const pathAttr = `${path}${attr}/`;
let alias2: string;
if (!aliasDict.hasOwnProperty(pathAttr)) {
alias2 = `${attr}_${count++}`;
alias2 = `${attr}_${number++}`;
assign(aliasDict, {
[pathAttr]: alias2,
});
@ -400,11 +401,6 @@ export abstract class SqlTranslator<ED extends EntityDict> {
}): void => {
const { attributes } = schema[entityName];
if (!isStat && attributes.hasOwnProperty('id') && !node.id) {
assign(node, {
id: 1,
});
}
Object.keys(node).forEach(
(attr) => {
const rel = judgeRelation(this.schema, entityName, attr);
@ -413,7 +409,7 @@ export abstract class SqlTranslator<ED extends EntityDict> {
let alias2: string;
if (!aliasDict.hasOwnProperty(pathAttr)) {
alias2 = `${rel}_${count++}`;
alias2 = `${rel}_${number++}`;
assign(aliasDict, {
[pathAttr]: alias2,
});
@ -435,7 +431,7 @@ export abstract class SqlTranslator<ED extends EntityDict> {
let alias2: string;
if (!aliasDict.hasOwnProperty(pathAttr)) {
alias2 = `${attr}_${count++}`;
alias2 = `${attr}_${number++}`;
assign(aliasDict, {
[pathAttr]: alias2,
});
@ -473,10 +469,11 @@ export abstract class SqlTranslator<ED extends EntityDict> {
projectionRefAlias,
filterRefAlias,
extraWhere,
currentNumber: number,
};
}
private translateComparison(attr: string, value: any, type: DataType): string {
private translateComparison(attr: string, value: any, type: DataType | Ref): string {
const SQL_OP: {
[op: string]: string,
} = {
@ -516,11 +513,17 @@ export abstract class SqlTranslator<ED extends EntityDict> {
return ' is null';
}
private translateEvaluation<T extends keyof ED>(attr: string, value: any, entity: T, alias: string, type: DataType): string {
private translateEvaluation<T extends keyof ED>(attr: string, value: any, entity: T, alias: string, type: DataType | Ref, initialNumber: number, refAlias: Record<string, string>): {
stmt: string;
currentNumber: number;
} {
switch (attr) {
case '$text': {
// fulltext search
return this.translateFullTextSearch(value, entity, alias);
return {
stmt: this.translateFullTextSearch(value, entity, alias),
currentNumber: initialNumber,
};
}
case '$in':
case '$nin': {
@ -531,7 +534,7 @@ export abstract class SqlTranslator<ED extends EntityDict> {
if (value instanceof Array) {
const values = value.map(
(v) => {
if (['varchar', 'char', 'text', 'nvarchar'].includes(type as string)) {
if (['varchar', 'char', 'text', 'nvarchar', 'ref'].includes(type as string)) {
return `'${v}'`;
}
else {
@ -540,27 +543,40 @@ export abstract class SqlTranslator<ED extends EntityDict> {
}
);
if (values.length > 0) {
return ` ${IN_OP[attr]}(${values.join(',')})`;
return {
stmt: ` ${IN_OP[attr]}(${values.join(',')})`,
currentNumber: initialNumber,
};
}
else {
if (attr === '$in') {
return ' in (null)';
return {
stmt: ' in (null)',
currentNumber: initialNumber,
};
}
else {
return ' is not null';
return {
stmt: ' is not null',
currentNumber: initialNumber,
};
}
}
}
else {
// sub query
return ` ${IN_OP[attr]}(${this.translateSelect(value.$entity, value)})`;
const {stmt: subQueryStmt, currentNumber } = this.translateSelectInner(value.entity, value, initialNumber, refAlias, undefined);
return {
stmt: ` ${IN_OP[attr]}(${subQueryStmt})`,
currentNumber,
};
}
}
default: {
assert('$between' === attr);
const values = value.map(
(v: string | number) => {
if (['varchar', 'char', 'text', 'nvarchar'].includes(type as string)) {
if (['varchar', 'char', 'text', 'nvarchar', 'ref'].includes(type as string)) {
return `'${v}'`;
}
else {
@ -568,26 +584,41 @@ export abstract class SqlTranslator<ED extends EntityDict> {
}
}
);
return ` between ${values[0]} and ${values[1]}`;
return {
stmt: ` between ${values[0]} and ${values[1]}`,
currentNumber: initialNumber,
};
}
}
}
private translateFilter<T extends keyof ED>(
entity: T,
selection: Pick<ED[T]['Selection'], 'filter' | 'hint'>,
aliasDict: Record<string, string>,
filterRefAlias: Record<string, string>,
filter?: ED[T]['Selection']['filter'],
extraWhere?: string): string {
initialNumber: number,
extraWhere?: string): {
stmt: string;
currentNumber: number;
} {
const { schema } = this;
const { filter, hint } = selection;
const translateInner = <E extends keyof ED>(entity2: E, path: string, filter2?: ED[E]['Selection']['filter'], type?: DataType): string => {
let currentNumber = initialNumber;
const translateInner = <E extends keyof ED>(entity2: E, path: string, filter2?: ED[E]['Selection']['filter'], type?: DataType | Ref): string => {
const alias = aliasDict[path];
const { attributes } = schema[entity2];
let whereText = '';
let whereText = type ? '' : this.getDefaultSelectFilter(alias, hint);
if (filter2) {
Object.keys(filter2).forEach(
(attr, idx) => {
const attrs = Object.keys(filter2).filter(
ele => !ele.startsWith('#')
);
attrs.forEach(
(attr) => {
if (whereText) {
whereText += ' and '
}
whereText + '(';
if (['$and', '$or', '$xor', '$not'].includes(attr)) {
let result = '';
@ -622,7 +653,7 @@ export abstract class SqlTranslator<ED extends EntityDict> {
}
else if (attr.toLowerCase().startsWith(EXPRESSION_PREFIX)) {
// expression
whereText += ` (${this.translateExpression(alias, filter2[attr], filterRefAlias)}) as ${attr}`;
whereText += ` (${this.translateExpression(alias, filter2[attr], filterRefAlias)})`;
}
else if (['$gt', '$gte', '$lt', '$lte', '$eq', '$ne', '$startsWith', '$endsWith', '$includes'].includes(attr)) {
whereText += this.translateComparison(attr, filter2[attr], type!);
@ -631,37 +662,52 @@ export abstract class SqlTranslator<ED extends EntityDict> {
whereText += this.translateElement(attr, filter2[attr]);
}
else if (['$text', '$in', '$nin', '$between'].includes(attr)) {
whereText += this.translateEvaluation(attr, filter2[attr], entity2, alias, type!);
const { stmt, currentNumber: cn2 } = this.translateEvaluation(attr, filter2[attr], entity2, alias, type!, initialNumber, filterRefAlias);
whereText += stmt;
currentNumber = cn2;
}
else {
assert(attributes.hasOwnProperty(attr));
const { type: type2, ref } = attributes[attr];
if (type2 === 'ref') {
whereText += ` ${translateInner(ref!, `${path}${attr}/`, filter2[attr])}`;
const rel = judgeRelation(this.schema, entity, attr);
if (rel === 2) {
whereText += ` ${translateInner(attr, `${path}${attr}/`, filter2[attr])}`;
}
else if (typeof filter2[attr] === 'object' && Object.keys(filter2[attr])[0] && Object.keys(filter2[attr])[0].startsWith('$')) {
whereText += ` \`${alias}\`.\`${attr}\` ${translateInner(entity2, path, filter2[attr], type2)}`
else if (typeof rel === 'string') {
whereText += ` ${translateInner(rel, `${path}${attr}/`, filter2[attr])}`;
}
else {
whereText += ` \`${alias}\`.\`${attr}\` = ${this.translateAttrValue(type2, filter2[attr])}`;
assert(attributes.hasOwnProperty(attr), `非法的属性${attr}`);
const { type: type2 } = attributes[attr];
// assert (type2 !== 'ref');
if (typeof filter2[attr] === 'object' && Object.keys(filter2[attr])[0] && Object.keys(filter2[attr])[0].startsWith('$')) {
whereText += ` \`${alias}\`.\`${attr}\` ${translateInner(entity2, path, filter2[attr], type2)}`
}
else {
whereText += ` \`${alias}\`.\`${attr}\` = ${this.translateAttrValue(type2, filter2[attr])}`;
}
}
}
whereText + ')';
if (idx < Object.keys(filter2).length - 1) {
whereText += ' and'
}
}
);
}
if (!whereText) {
whereText = 'true'; // 如果为空就赋一个永真条件以便处理and
}
return whereText;
};
const where = translateInner(entity, './', filter);
if (extraWhere && where) {
return `${extraWhere} and ${where}`;
return {
stmt: `${extraWhere} and ${where}`,
currentNumber,
};
}
return extraWhere || where;
return {
stmt: extraWhere || where,
currentNumber,
};
}
private translateSorter<T extends keyof ED>(entity: T, sorter: ED[T]['Selection']['sorter'], aliasDict: Record<string, string>): string {
@ -718,7 +764,16 @@ export abstract class SqlTranslator<ED extends EntityDict> {
let projText = '';
let prefix = path.slice(2).replace(/\//g, '.');
Object.keys(projection2).forEach(
const attrs = Object.keys(projection2).filter(
(attr) => {
if (attr.toLowerCase().startsWith(EXPRESSION_PREFIX)) {
return true;
}
const rel = judgeRelation(this.schema, entity2, attr);
return [1, 2].includes(rel as number) || typeof rel === 'string';
}
);
attrs.forEach(
(attr, idx) => {
if (attr.toLowerCase().startsWith(EXPRESSION_PREFIX)) {
const exprText = this.translateExpression(alias, projection2[attr], projectionRefAlias);
@ -732,8 +787,7 @@ export abstract class SqlTranslator<ED extends EntityDict> {
else if (rel === 2) {
projText += translateInner(attr, projection2[attr], `${path}${attr}/`);
}
else {
assert(rel === 0 || rel === 1);
else if (rel === 1) {
const { type } = attributes[attr];
if (projection2[attr] === 1) {
projText += ` ${this.translateAttrProjection(type as DataType, alias, attr)} as \`${prefix}${attr}\``;
@ -744,7 +798,7 @@ export abstract class SqlTranslator<ED extends EntityDict> {
}
}
}
if (idx < Object.keys(projection2).length - 1) {
if (idx < attrs.length - 1) {
projText += ',';
}
}
@ -756,44 +810,57 @@ export abstract class SqlTranslator<ED extends EntityDict> {
return translateInner(entity, projection, './');
}
translateSelect<T extends keyof ED>(entity: T, selection: ED[T]['Selection'], params?: SelectParams): string {
private translateSelectInner<T extends keyof ED>(entity: T, selection: ED[T]['Selection'], initialNumber: number, refAlias: Record<string, string>, params?: SelectParams): {
stmt: string;
currentNumber: number;
} {
const { data, filter, sorter, indexFrom, count } = selection;
const { from: fromText, aliasDict, projectionRefAlias, extraWhere, filterRefAlias } = this.analyzeJoin(entity, {
const { from: fromText, aliasDict, projectionRefAlias, extraWhere, filterRefAlias, currentNumber } = this.analyzeJoin(entity, {
projection: data,
filter,
sorter,
});
}, initialNumber);
assert(intersection(keys(refAlias), keys(filterRefAlias)).length === 0, 'filter中的#node结点定义有重复');
assign(refAlias, filterRefAlias);
const projText = this.translateProjection(entity, data, aliasDict, projectionRefAlias);
const filterText = this.translateFilter(entity, aliasDict, filterRefAlias, filter, extraWhere);
const { stmt: filterText, currentNumber: currentNumber2 } = this.translateFilter(entity, selection, aliasDict, refAlias, currentNumber, extraWhere);
const sorterText = sorter && this.translateSorter(entity, sorter, aliasDict);
return this.populateSelectStmt(projText, fromText, aliasDict, filterText, sorterText, indexFrom, count, params);
return {
stmt: this.populateSelectStmt(projText, fromText, selection, aliasDict, filterText, sorterText, indexFrom, count),
currentNumber: currentNumber2,
};
}
translateCount<T extends keyof ED>(entity: T, selection: Omit<ED[T]['Selection'], 'data' | 'sorter' | 'action'>, params?: SelectParams): string {
translateSelect<T extends keyof ED>(entity: T, selection: ED[T]['Selection'], params?: SelectParams): string {
const { stmt } = this.translateSelectInner(entity, selection, 1, {}, params);
return stmt;
}
translateCount<T extends keyof ED>(entity: T, selection: Pick<ED[T]['Selection'], 'filter'>, params?: SelectParams): string {
const { filter } = selection;
const { from: fromText, aliasDict, extraWhere, filterRefAlias } = this.analyzeJoin(entity, {
const { from: fromText, aliasDict, extraWhere, filterRefAlias, currentNumber } = this.analyzeJoin(entity, {
filter,
});
const projText = 'count(1)';
const filterText = this.translateFilter(entity, aliasDict, filterRefAlias, filter, extraWhere);
const { stmt: filterText } = this.translateFilter(entity, selection as ED[T]['Selection'], aliasDict, filterRefAlias, currentNumber, extraWhere);
return this.populateSelectStmt(projText, fromText, aliasDict, filterText, undefined, undefined, undefined, params);
return this.populateSelectStmt(projText, fromText, selection as ED[T]['Selection'], aliasDict, filterText, undefined, undefined, undefined);
}
translateRemove<T extends keyof ED>(entity: T, operation: ED[T]['Remove'], params?: SelectParams): string {
const { filter, sorter, indexFrom, count } = operation;
const { aliasDict, filterRefAlias, extraWhere, from: fromText } = this.analyzeJoin(entity, { filter, sorter });
const { aliasDict, filterRefAlias, extraWhere, from: fromText, currentNumber } = this.analyzeJoin(entity, { filter, sorter });
const alias = aliasDict['./'];
const filterText = this.translateFilter(entity, aliasDict, filterRefAlias, filter, extraWhere);
const { stmt: filterText } = this.translateFilter(entity, operation, aliasDict, filterRefAlias, currentNumber, extraWhere);
const sorterText = sorter && sorter.length > 0 ? this.translateSorter(entity, sorter, aliasDict) : undefined;
@ -803,7 +870,7 @@ export abstract class SqlTranslator<ED extends EntityDict> {
translateUpdate<T extends keyof ED>(entity: T, operation: ED[T]['Update'], params?: any): string {
const { attributes } = this.schema[entity];
const { filter, sorter, indexFrom, count, data } = operation;
const { aliasDict, filterRefAlias, extraWhere, from: fromText } = this.analyzeJoin(entity, { filter, sorter });
const { aliasDict, filterRefAlias, extraWhere, from: fromText, currentNumber } = this.analyzeJoin(entity, { filter, sorter });
const alias = aliasDict['./'];
@ -817,7 +884,7 @@ export abstract class SqlTranslator<ED extends EntityDict> {
updateText += `\`${alias}\`.\`${attr}\` = ${value}`;
}
const filterText = this.translateFilter(entity, aliasDict, filterRefAlias, filter, extraWhere);
const { stmt: filterText } = this.translateFilter(entity, operation, aliasDict, filterRefAlias, currentNumber, extraWhere);
const sorterText = sorter && this.translateSorter(entity, sorter, aliasDict);
return this.populateUpdateStmt(updateText, fromText, aliasDict, filterText, sorterText, indexFrom, count, params);

View File

@ -1,3 +1,4 @@
import assert from 'assert';
import { UniversalContext } from 'oak-domain/lib/store/UniversalContext';
import { v4 } from 'uuid';
import { MysqlStore } from '../src/MySQL/store';
@ -54,14 +55,714 @@ describe('test mysqlstore', function() {
env: {
type: 'web',
},
applicationId: await v4(),
applicationId: v4(),
userId: v4(),
entity: 'mobile',
entityId: v4(),
}
}]
} as EntityDict['user']['Create']['data']
} as EntityDict['user']['Create'], context);
}
}, context);
});
it('test update', async () => {
const context = new UniversalContext(store);
const tokenId = v4();
await store.operate('user', {
action: 'create',
data: {
id: v4(),
name: 'xxxc',
nickname: 'ddd',
token$player: [{
action: 'create',
data: {
id: tokenId,
env: {
type: 'web',
},
applicationId: v4(),
userId: v4(),
entity: 'mobile',
entityId: v4(),
}
}]
}
}, context);
await store.operate('token', {
action: 'update',
filter: {
id: tokenId,
},
data: {
player: {
action: 'activate',
data: {
name: 'xcxcxc0903'
},
}
}
}, context);
});
it('test delete', async () => {
const context = new UniversalContext(store);
const tokenId = v4();
await store.operate('user', {
action: 'create',
data: {
id: v4(),
name: 'xxxc',
nickname: 'ddd',
token$player: [{
action: 'create',
data: {
id: tokenId,
env: {
type: 'web',
},
applicationId: v4(),
userId: v4(),
entity: 'mobile',
entityId: v4(),
}
}]
}
}, context);
await store.operate('token', {
action: 'remove',
filter: {
id: tokenId,
},
data: {
player: {
action: 'update',
data: {
name: 'xcxcxc0902'
},
}
}
}, context);
});
it('test delete2', async () => {
// 这个例子暂在mysql上过不去先放着吧
const context = new UniversalContext(store);
const tokenId = v4();
await store.operate('user', {
action: 'create',
data: {
id: v4(),
name: 'xxxc',
nickname: 'ddd',
token$player: [{
action: 'create',
data: {
id: tokenId,
env: {
type: 'web',
},
applicationId: v4(),
userId: v4(),
entity: 'mobile',
entityId: v4(),
}
}]
}
}, context);
await store.operate('user', {
action: 'remove',
filter: {
id: tokenId,
},
data: {
ref: {
action: 'remove',
data: {},
}
},
}, context);
});
it('[1.1]子查询', async () => {
const context = new UniversalContext(store);
await store.operate('user', {
action: 'create',
data: {
id: v4(),
name: 'xc',
nickname: 'xc',
}
}, context);
/**
*
* store.ts中translateAttribute函数里$in的分支代码
* by Xc
*/
process.env.NODE_ENV = 'development';
const rows = await store.select('user', {
data: {
id: 1,
name: 1,
nickname: 1,
},
filter: {
id: {
$in: {
entity: 'token',
data: {
userId: 1,
},
filter: {
entity: 'mobile',
}
},
}
},
}, context);
process.env.NODE_ENV = undefined;
// console.log(rows);
assert(rows.result.length === 0);
});
it('[1.2]行内属性上的表达式', async () => {
const context = new UniversalContext(store);
const id = v4();
await store.operate('user', {
action: 'create',
data: {
id,
name: 'xc',
nickname: 'xc',
}
}, context);
process.env.NODE_ENV = 'development';
const { result: users } = await store.select('user', {
data: {
id: 1,
name: 1,
nickname: 1,
},
filter: {
// '#id': 'node-123',
$expr: {
$eq: [{
'#attr': 'name',
}, {
"#attr": 'nickname',
}]
},
id,
},
}, context);
process.env.NODE_ENV = undefined;
assert(users.length === 1);
});
it('[1.3]跨filter结点的表达式', async () => {
const context = new UniversalContext(store);
const id1 = v4();
const id2 = v4();
await store.operate('application', {
action: 'create',
data: [{
id: id1,
name: 'test',
description: 'ttttt',
type: 'web',
config: {
type: 'web',
domain: 'http://www.tt.com',
},
system: {
action: 'create',
data: {
id: 'bbb',
name: 'systest',
description: 'aaaaa',
config: {},
}
}
}, {
id: id2,
name: 'test2',
description: 'ttttt2',
type: 'web',
config: {
type: 'web',
domain: 'http://www.tt.com',
},
system: {
action: 'create',
data: {
id: 'ccc',
name: 'test2',
description: 'aaaaa2',
config: {},
}
}
}]
}, context);
const { result: applications } = await store.select('application', {
data: {
id: 1,
name: 1,
systemId: 1,
system: {
id: 1,
name: 1,
}
},
filter: {
$expr: {
$startsWith: [
{
"#refAttr": 'name',
"#refId": 'node-1',
},
{
"#attr": 'name',
}
]
},
system: {
"#id": 'node-1',
},
id: id2,
},
sorter: [
{
$attr: {
system: {
name: 1,
}
},
$direction: 'asc',
}
]
}, context);
console.log(applications);
assert(applications.length === 1 && applications[0].id === id2);
});
it('[1.4]跨filter子查询的表达式', async () => {
const context = new UniversalContext(store);
await store.operate('application', {
action: 'create',
data: [{
id: 'aaa',
name: 'test',
description: 'ttttt',
type: 'web',
config: {
type: 'web',
domain: 'http://www.tt.com',
},
system: {
action: 'create',
data: {
id: 'bbb',
name: 'systest',
description: 'aaaaa',
config: {},
}
}
}, {
id: 'aaa2',
name: 'test2',
description: 'ttttt2',
type: 'web',
config: {
type: 'web',
domain: 'http://www.tt.com',
},
system: {
action: 'create',
data: {
id: 'ccc',
name: 'test2',
description: 'aaaaa2',
config: {},
}
}
}]
}, context);
process.env.NODE_ENV = 'development';
let systems = await store.select('system', {
data: {
id: 1,
name: 1,
},
filter: {
"#id": 'node-1',
id: {
$nin: {
entity: 'application',
data: {
systemId: 1,
},
filter: {
$expr: {
$eq: [
{
"#attr": 'name',
},
{
'#refId': 'node-1',
"#refAttr": 'name',
}
]
},
'#id': 'node-2',
}
},
}
},
sorter: [
{
$attr: {
name: 1,
},
$direction: 'asc',
}
]
}, context);
assert(systems.result.length === 1 && systems.result[0].id === 'bbb');
systems = await store.select('system', {
data: {
id: 1,
name: 1,
},
filter: {
"#id": 'node-1',
id: {
$in: {
entity: 'application',
data: {
systemId: 1,
},
filter: {
$expr: {
$eq: [
{
"#attr": 'name',
},
{
'#refId': 'node-1',
"#refAttr": 'name',
}
]
},
}
},
}
},
sorter: [
{
$attr: {
name: 1,
},
$direction: 'asc',
}
]
}, context);
process.env.NODE_ENV = undefined;
assert(systems.result.length === 1 && systems.result[0].id === 'ccc');
});
it('[1.5]projection中的跨结点表达式', async () => {
const context = new UniversalContext(store);
await store.operate('application', {
action: 'create',
data: [{
id: 'aaa',
name: 'test',
description: 'ttttt',
type: 'web',
config: {
type: 'web',
domain: 'http://www.tt.com',
},
system: {
action: 'create',
data: {
id: 'bbb',
name: 'systest',
description: 'aaaaa',
config: {},
}
}
}, {
id: 'aaa2',
name: 'test2',
description: 'ttttt2',
type: 'web',
config: {
type: 'web',
domain: 'http://www.tt.com',
},
system: {
action: 'create',
data: {
id: 'ccc',
name: 'test2',
description: 'aaaaa2',
config: {},
}
}
}]
}, context);
let applications = await store.select('application', {
data: {
"#id": 'node-1',
id: 1,
name: 1,
system: {
id: 1,
name: 1,
$expr: {
$eq: [
{
"#attr": 'name',
},
{
'#refId': 'node-1',
"#refAttr": 'name',
}
]
},
}
},
}, context);
// console.log(applications);
assert(applications.result.length === 2);
applications.result.forEach(
(app) => {
assert(app.id === 'aaa' && app.system!.$expr === false
|| app.id === 'aaa2' && app.system!.$expr === true);
}
);
const applications2 = await store.select('application', {
data: {
$expr: {
$eq: [
{
"#attr": 'name',
},
{
'#refId': 'node-1',
"#refAttr": 'name',
}
]
},
id: 1,
name: 1,
system: {
"#id": 'node-1',
id: 1,
name: 1,
}
},
}, context);
console.log(applications2);
// assert(applications.length === 2);
applications2.result.forEach(
(app) => {
assert(app.id === 'aaa' && app.$expr === false
|| app.id === 'aaa2' && app.$expr === true);
}
);
});
// 这个貌似目前支持不了 by Xc
it('[1.6]projection中的一对多跨结点表达式', async () => {
const context = new UniversalContext(store);
await store.operate('system', {
action: 'create',
data: {
id: 'bbb',
name: 'test2',
description: 'aaaaa',
config: {},
application$system: [{
action: 'create',
data: [
{
id: 'aaa',
name: 'test',
description: 'ttttt',
type: 'web',
config: {
type: 'web',
domain: 'http://www.tt.com',
},
},
{
id: 'aaa2',
name: 'test2',
description: 'ttttt2',
type: 'wechatMp',
config: {
type: 'web',
domain: 'http://www.tt.com',
},
}
]
}]
}
}, context);
const systems = await store.select('system', {
data: {
"#id": 'node-1',
id: 1,
name: 1,
application$system: {
$entity: 'application',
data: {
id: 1,
name: 1,
$expr: {
$eq: [
{
"#attr": 'name',
},
{
'#refId': 'node-1',
"#refAttr": 'name',
}
]
},
$expr2: {
'#refId': 'node-1',
"#refAttr": 'id',
}
}
},
},
}, context);
// console.log(systems);
assert(systems.result.length === 1);
const [ system ] = systems.result;
const { application$system: applications } = system;
assert(applications!.length === 2);
applications!.forEach(
(ele) => {
assert(ele.id === 'aaa' && ele.$expr === false && ele.$expr2 === 'bbb'
|| ele.id === 'aaa2' && ele.$expr === true && ele.$expr2 === 'bbb');
}
);
});
it('[1.7]事务性测试', async () => {
const context = new UniversalContext(store);
await store.operate('system', {
action: 'create',
data: {
id: 'bbb',
name: 'test2',
description: 'aaaaa',
config: {},
application$system: [{
action: 'create',
data: [
{
id: 'aaa',
name: 'test',
description: 'ttttt',
type: 'web',
config: {
type: 'web',
domain: 'http://www.tt.com',
},
},
{
id: 'aaa2',
name: 'test2',
description: 'ttttt2',
type: 'wechatMp',
config: {
type: 'web',
domain: 'http://www.tt.com',
},
}
]
}]
}
}, context);
await context.begin();
const systems = await store.select('system', {
data: {
id: 1,
name: 1,
application$system: {
$entity: 'application',
data: {
id: 1,
name: 1,
}
},
},
}, context);
assert(systems.result.length === 1 && systems.result[0].application$system!.length === 2);
await store.operate('application', {
action: 'remove',
data: {},
filter: {
id: 'aaa',
}
}, context);
const systems2 = await store.select('system', {
data: {
id: 1,
name: 1,
application$system: {
$entity: 'application',
data: {
id: 1,
name: 1,
}
},
},
}, context);
assert(systems2.result.length === 1 && systems2.result[0].application$system!.length === 1);
await context.rollback();
const systems3 = await store.select('system', {
data: {
id: 1,
name: 1,
application$system: {
$entity: 'application',
data: {
id: 1,
name: 1,
}
},
},
}, context);
assert(systems3.result.length === 1 && systems3.result[0].application$system!.length === 2);
});
after(() => {

View File

@ -54,4 +54,5 @@ describe('test MysqlTranslator', function() {
});
console.log(sql);
});
});