projection翻译时对无用metadata的过滤
This commit is contained in:
parent
b7c64e67c9
commit
b9b9145705
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
"use strict";
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export interface DbContext
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './MySQL/store';
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -54,4 +54,5 @@ describe('test MysqlTranslator', function() {
|
|||
});
|
||||
console.log(sql);
|
||||
});
|
||||
|
||||
});
|
||||
Loading…
Reference in New Issue