@digitalwalletcorp/sql-builder 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -0
- package/lib/abstract-syntax-tree.d.ts +62 -0
- package/lib/abstract-syntax-tree.js +347 -0
- package/lib/abstract-syntax-tree.js.map +1 -0
- package/lib/common.d.ts +8 -0
- package/lib/common.js +24 -0
- package/lib/common.js.map +1 -0
- package/lib/sql-builder.d.ts +39 -0
- package/lib/sql-builder.js +209 -72
- package/lib/sql-builder.js.map +1 -1
- package/package.json +3 -2
- package/src/abstract-syntax-tree.ts +319 -0
- package/src/common.ts +19 -0
- package/src/sql-builder.ts +57 -79
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import * as common from './common';
|
|
2
|
+
|
|
3
|
+
type Token =
|
|
4
|
+
| { type: 'IDENTIFIER'; value: string } // 例: param, length
|
|
5
|
+
| { type: 'OPERATOR'; value: string } // 例: >, &&, ===, !=
|
|
6
|
+
| { type: 'NUMBER'; value: number } // 例: 100.123
|
|
7
|
+
| { type: 'BOOLEAN'; value: boolean } // 例: true, false
|
|
8
|
+
| { type: 'STRING'; value: string } // 例: 'abc'
|
|
9
|
+
| { type: 'NULL'; value: null }
|
|
10
|
+
| { type: 'UNDEFINED', value: undefined }
|
|
11
|
+
| { type: 'PARENTHESIS'; value: '(' | ')' };
|
|
12
|
+
|
|
13
|
+
// 演算子の優先順位と結合性 (より長い演算子を先に定義)
|
|
14
|
+
const PRECEDENCE: {
|
|
15
|
+
[op: string]: {
|
|
16
|
+
precedence: number;
|
|
17
|
+
associativity: 'left' | 'right'
|
|
18
|
+
}
|
|
19
|
+
} = {
|
|
20
|
+
'||': { precedence: 1, associativity: 'left' },
|
|
21
|
+
'&&': { precedence: 2, associativity: 'left' },
|
|
22
|
+
'==': { precedence: 3, associativity: 'left' },
|
|
23
|
+
'!=': { precedence: 3, associativity: 'left' },
|
|
24
|
+
'===': { precedence: 3, associativity: 'left' },
|
|
25
|
+
'!==': { precedence: 3, associativity: 'left' },
|
|
26
|
+
'<': { precedence: 4, associativity: 'left' },
|
|
27
|
+
'<=': { precedence: 4, associativity: 'left' },
|
|
28
|
+
'>': { precedence: 4, associativity: 'left' },
|
|
29
|
+
'>=': { precedence: 4, associativity: 'left' },
|
|
30
|
+
'!': { precedence: 5, associativity: 'right' } // 単項演算子は高優先度
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* SQLBuilderの条件文(*IF*)で指定された条件文字列を解析するためのAST。
|
|
35
|
+
* JavaScriptの文法をカバーするものではなく、SQLBuilderで利用可能な限定的な構文のみサポートする。
|
|
36
|
+
*/
|
|
37
|
+
export class AbstractSyntaxTree {
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 与えられた条件文字列を構文解析し、entityに対する条件として成立するか評価する
|
|
41
|
+
*
|
|
42
|
+
* @param {string} condition "params != null && params.length > 10" のような条件
|
|
43
|
+
* @param {Record<string, any>} entity
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
public evaluateCondition(condition: string, entity: Record<string, any>): boolean {
|
|
47
|
+
try {
|
|
48
|
+
const tokens = this.tokenize(condition); // トークン化
|
|
49
|
+
const rpnTokens = this.shuntingYard(tokens); // RPN変換
|
|
50
|
+
const result = this.evaluateRpn(rpnTokens, entity); // 評価
|
|
51
|
+
return result;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error('Error evaluating condition:', condition, entity, error);
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 与えられた条件文字列をトークンに分割する
|
|
60
|
+
*
|
|
61
|
+
* @param {string} condition
|
|
62
|
+
* @returns {Token[]}
|
|
63
|
+
*/
|
|
64
|
+
public tokenize(condition: string): Token[] {
|
|
65
|
+
const tokens: Token[] = [];
|
|
66
|
+
let i = 0;
|
|
67
|
+
while (i < condition.length) {
|
|
68
|
+
const char = condition[i];
|
|
69
|
+
// 空白をスキップ
|
|
70
|
+
if (/\s/.test(char)) {
|
|
71
|
+
i++;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 演算子 (長いものからチェック)
|
|
76
|
+
// 3桁定義
|
|
77
|
+
const chunk3 = condition.substring(i, i + 3);
|
|
78
|
+
switch (chunk3) {
|
|
79
|
+
case '===':
|
|
80
|
+
case '!==':
|
|
81
|
+
tokens.push({ type: 'OPERATOR', value: chunk3 });
|
|
82
|
+
i += 3;
|
|
83
|
+
continue;
|
|
84
|
+
default:
|
|
85
|
+
}
|
|
86
|
+
// 2桁定義
|
|
87
|
+
const chunk2 = condition.substring(i, i + 2);
|
|
88
|
+
switch (chunk2) {
|
|
89
|
+
case '==':
|
|
90
|
+
case '!=':
|
|
91
|
+
case '<=':
|
|
92
|
+
case '>=':
|
|
93
|
+
case '&&':
|
|
94
|
+
case '||':
|
|
95
|
+
tokens.push({ type: 'OPERATOR', value: chunk2 });
|
|
96
|
+
i += 2;
|
|
97
|
+
continue;
|
|
98
|
+
default:
|
|
99
|
+
}
|
|
100
|
+
// 1桁定義
|
|
101
|
+
const chunk1 = char;
|
|
102
|
+
switch (chunk1) {
|
|
103
|
+
case '>':
|
|
104
|
+
case '<':
|
|
105
|
+
case '!':
|
|
106
|
+
case '=':
|
|
107
|
+
tokens.push({ type: 'OPERATOR', value: chunk1 });
|
|
108
|
+
i += 1;
|
|
109
|
+
continue;
|
|
110
|
+
// case '.':
|
|
111
|
+
// tokens.push({ type: 'IDENTIFIER', value: chunk1 }); // '.'も識別子の一部として扱う
|
|
112
|
+
// i += 1;
|
|
113
|
+
// continue;
|
|
114
|
+
case '(':
|
|
115
|
+
case ')':
|
|
116
|
+
tokens.push({ type: 'PARENTHESIS', value: chunk1 });
|
|
117
|
+
i += 1;
|
|
118
|
+
continue;
|
|
119
|
+
default:
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const reststring = condition.substring(i); // 現在のインデックスから末尾までの文字列
|
|
123
|
+
// 数値リテラル
|
|
124
|
+
const numMatch = reststring.match(/^-?\d+(\.\d+)?/);
|
|
125
|
+
if (numMatch) {
|
|
126
|
+
tokens.push({ type: 'NUMBER', value: parseFloat(numMatch[0]) });
|
|
127
|
+
i += numMatch[0].length;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 文字列リテラル
|
|
132
|
+
switch (chunk1) {
|
|
133
|
+
case '\'':
|
|
134
|
+
case '"':
|
|
135
|
+
const quote = chunk1;
|
|
136
|
+
let j = i + 1;
|
|
137
|
+
let strValue = '';
|
|
138
|
+
while (j < condition.length && condition[j] !== quote) {
|
|
139
|
+
// エスケープ文字の処理 (\' や \\) は必要に応じて追加
|
|
140
|
+
if (condition[j] === '\\' && j + 1 < condition.length) {
|
|
141
|
+
strValue += condition[j+1];
|
|
142
|
+
j += 2;
|
|
143
|
+
} else {
|
|
144
|
+
strValue += condition[j];
|
|
145
|
+
j++;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (condition[j] === quote) {
|
|
149
|
+
tokens.push({ type: 'STRING', value: strValue });
|
|
150
|
+
i = j + 1;
|
|
151
|
+
continue;
|
|
152
|
+
} else {
|
|
153
|
+
// クォートが閉じられていない
|
|
154
|
+
throw new Error('Unterminated string literal');
|
|
155
|
+
}
|
|
156
|
+
default:
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 識別子 (変数名, true, false, null, undefined, length)
|
|
160
|
+
const identMatch = reststring.match(/^[a-zA-Z_][a-zA-Z0-9_.]*/); // ドットを含む識別子
|
|
161
|
+
if (identMatch) {
|
|
162
|
+
const ident = identMatch[0];
|
|
163
|
+
switch (ident) {
|
|
164
|
+
case 'true':
|
|
165
|
+
tokens.push({ type: 'BOOLEAN', value: true });
|
|
166
|
+
break;
|
|
167
|
+
case 'false':
|
|
168
|
+
tokens.push({ type: 'BOOLEAN', value: false });
|
|
169
|
+
break;
|
|
170
|
+
case 'null':
|
|
171
|
+
tokens.push({ type: 'NULL', value: null });
|
|
172
|
+
break;
|
|
173
|
+
case 'undefined':
|
|
174
|
+
tokens.push({ type: 'UNDEFINED', value: undefined });
|
|
175
|
+
break;
|
|
176
|
+
default:
|
|
177
|
+
tokens.push({ type: 'IDENTIFIER', value: ident }); // プロパティ名
|
|
178
|
+
}
|
|
179
|
+
i += ident.length;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 未知の文字
|
|
184
|
+
throw new Error(`Unexpected character in condition: ${char} at index ${i}`);
|
|
185
|
+
}
|
|
186
|
+
return tokens;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Shunting Yardアルゴリズムで構文を逆ポーランド記法(Reverse Polish Notation)に変換する
|
|
191
|
+
*
|
|
192
|
+
* @param {Token[]} tokens
|
|
193
|
+
* @returns {Token[]}
|
|
194
|
+
*/
|
|
195
|
+
private shuntingYard(tokens: Token[]): Token[] {
|
|
196
|
+
const output: Token[] = [];
|
|
197
|
+
const operatorStack: (Token & { value: string })[] = [];
|
|
198
|
+
|
|
199
|
+
for (const token of tokens) {
|
|
200
|
+
switch (token.type) {
|
|
201
|
+
case 'NUMBER':
|
|
202
|
+
case 'BOOLEAN':
|
|
203
|
+
case 'NULL':
|
|
204
|
+
case 'UNDEFINED':
|
|
205
|
+
case 'STRING':
|
|
206
|
+
case 'IDENTIFIER':
|
|
207
|
+
output.push(token);
|
|
208
|
+
break;
|
|
209
|
+
case 'OPERATOR':
|
|
210
|
+
const op1 = token;
|
|
211
|
+
while (operatorStack.length) {
|
|
212
|
+
const op2 = operatorStack[operatorStack.length - 1];
|
|
213
|
+
|
|
214
|
+
// 括弧内は処理しない
|
|
215
|
+
if (op2.value === '(') {
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 優先順位のルールに従う
|
|
220
|
+
if (PRECEDENCE[op1.value].associativity === 'left'
|
|
221
|
+
&& PRECEDENCE[op1.value].precedence <= PRECEDENCE[op2.value].precedence) {
|
|
222
|
+
output.push(operatorStack.pop()!);
|
|
223
|
+
} else {
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
operatorStack.push(op1);
|
|
228
|
+
break;
|
|
229
|
+
case 'PARENTHESIS':
|
|
230
|
+
if (token.value === '(') {
|
|
231
|
+
operatorStack.push(token);
|
|
232
|
+
} else if (token.value === ')') {
|
|
233
|
+
let foundLeftParen = false;
|
|
234
|
+
while (operatorStack.length > 0) {
|
|
235
|
+
const op = operatorStack.pop()!;
|
|
236
|
+
if ('value' in op && op.value === '(') {
|
|
237
|
+
foundLeftParen = true;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
output.push(op);
|
|
241
|
+
}
|
|
242
|
+
if (!foundLeftParen) {
|
|
243
|
+
throw new Error('Mismatched parentheses');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
// default:
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
while (operatorStack.length) {
|
|
252
|
+
const op = operatorStack.pop()!;
|
|
253
|
+
if ('value' in op && (op.value === '(' || op.value === ')')) {
|
|
254
|
+
throw new Error('Mismatched parentheses');
|
|
255
|
+
}
|
|
256
|
+
output.push(op);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return output;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* 逆ポーランド記法(Reverse Polish Notation)のトークンを評価する
|
|
264
|
+
*
|
|
265
|
+
* @param {Token[]} rpnTokens
|
|
266
|
+
* @param {Record<string, any>} entity
|
|
267
|
+
* @returns {boolean}
|
|
268
|
+
*/
|
|
269
|
+
private evaluateRpn(rpnTokens: Token[], entity: Record<string, any>): boolean {
|
|
270
|
+
const stack: any[] = [];
|
|
271
|
+
|
|
272
|
+
for (const token of rpnTokens) {
|
|
273
|
+
switch (token.type) {
|
|
274
|
+
case 'NUMBER':
|
|
275
|
+
case 'BOOLEAN':
|
|
276
|
+
case 'STRING':
|
|
277
|
+
case 'NULL':
|
|
278
|
+
case 'UNDEFINED':
|
|
279
|
+
stack.push(token.value);
|
|
280
|
+
break;
|
|
281
|
+
case 'IDENTIFIER':
|
|
282
|
+
stack.push(common.getProperty(entity, token.value));
|
|
283
|
+
break;
|
|
284
|
+
case 'OPERATOR':
|
|
285
|
+
// 単項演算子 '!'
|
|
286
|
+
if (token.value === '!') {
|
|
287
|
+
const operand = stack.pop();
|
|
288
|
+
stack.push(!operand);
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 二項演算子
|
|
293
|
+
const right = stack.pop();
|
|
294
|
+
const left = stack.pop();
|
|
295
|
+
|
|
296
|
+
switch (token.value) {
|
|
297
|
+
case '==': stack.push(left == right); break;
|
|
298
|
+
case '!=': stack.push(left != right); break;
|
|
299
|
+
case '===': stack.push(left === right); break;
|
|
300
|
+
case '!==': stack.push(left !== right); break;
|
|
301
|
+
case '<': stack.push(left < right); break;
|
|
302
|
+
case '<=': stack.push(left <= right); break;
|
|
303
|
+
case '>': stack.push(left > right); break;
|
|
304
|
+
case '>=': stack.push(left >= right); break;
|
|
305
|
+
case '&&': stack.push(left && right); break;
|
|
306
|
+
case '||': stack.push(left || right); break;
|
|
307
|
+
default: throw new Error(`Unknown operator: ${token.value}`);
|
|
308
|
+
}
|
|
309
|
+
break;
|
|
310
|
+
// default:
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (stack.length !== 1) {
|
|
315
|
+
throw new Error('Invalid expression');
|
|
316
|
+
}
|
|
317
|
+
return !!stack[0];
|
|
318
|
+
}
|
|
319
|
+
}
|
package/src/common.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* entityで指定したオブジェクトからドットで連結されたプロパティキーに該当する値を取得する
|
|
3
|
+
*
|
|
4
|
+
* @param {Record<string, any>} entity
|
|
5
|
+
* @param {string} property
|
|
6
|
+
* @returns {any}
|
|
7
|
+
*/
|
|
8
|
+
export function getProperty(entity: Record<string, any>, property: string): any {
|
|
9
|
+
const propertyPath = property.split('.');
|
|
10
|
+
let value = entity;
|
|
11
|
+
for (const prop of propertyPath) {
|
|
12
|
+
if (value && value.hasOwnProperty(prop)) {
|
|
13
|
+
value = value[prop];
|
|
14
|
+
} else {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
package/src/sql-builder.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import * as common from './common';
|
|
2
|
+
import { AbstractSyntaxTree } from './abstract-syntax-tree';
|
|
3
|
+
|
|
1
4
|
type TagType = 'BEGIN' | 'IF' | 'FOR' | 'END' | 'BIND';
|
|
2
5
|
type ExtractValueType<T extends 'string' | 'array' | 'object'>
|
|
3
6
|
= T extends 'string'
|
|
@@ -164,57 +167,56 @@ export class SQLBuilder {
|
|
|
164
167
|
/**
|
|
165
168
|
* 「\/* *\/」で囲まれたすべての箇所を抽出
|
|
166
169
|
*/
|
|
167
|
-
const
|
|
170
|
+
const matches = template.matchAll(this.REGEX_TAG_PATTERN);
|
|
168
171
|
|
|
169
172
|
// まず最初にREGEX_TAG_PATTERNで解析した情報をそのままフラットにTagContextの配列に格納
|
|
170
173
|
let pos = 0;
|
|
171
174
|
const tagContexts: TagContext[] = [];
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
175
|
+
for (const match of matches) {
|
|
176
|
+
const matchContent = match[0];
|
|
177
|
+
const index = match.index;
|
|
178
|
+
pos = index + 1;
|
|
179
|
+
const tagContext: TagContext = {
|
|
180
|
+
type: 'BIND', // ダミーの初期値。後続処理で適切なタイプに変更する。
|
|
181
|
+
match: matchContent,
|
|
182
|
+
contents: '',
|
|
183
|
+
startIndex: index,
|
|
184
|
+
endIndex: index + matchContent.length,
|
|
185
|
+
sub: [],
|
|
186
|
+
parent: null,
|
|
187
|
+
status: 0
|
|
188
|
+
};
|
|
189
|
+
switch (true) {
|
|
190
|
+
case matchContent === '/*BEGIN*/': {
|
|
191
|
+
tagContext.type = 'BEGIN';
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
case matchContent.startsWith('/*IF'): {
|
|
195
|
+
tagContext.type = 'IF';
|
|
196
|
+
const contentMatcher = matchContent.match(/^\/\*IF\s+(.*?)\*\/$/);
|
|
197
|
+
tagContext.contents = contentMatcher && contentMatcher[1] || '';
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case matchContent.startsWith('/*FOR'): {
|
|
201
|
+
tagContext.type = 'FOR';
|
|
202
|
+
const contentMatcher = matchContent.match(/^\/\*FOR\s+(.*?)\*\/$/);
|
|
203
|
+
tagContext.contents = contentMatcher && contentMatcher[1] || '';
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
case matchContent === '/*END*/': {
|
|
207
|
+
tagContext.type = 'END';
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
default: {
|
|
211
|
+
tagContext.type = 'BIND';
|
|
212
|
+
const contentMatcher = matchContent.match(/\/\*(.*?)\*\//);
|
|
213
|
+
tagContext.contents = contentMatcher && contentMatcher[1] || '';
|
|
214
|
+
// ダミー値の終了位置をendIndexに設定
|
|
215
|
+
const dummyEndIndex = this.getDummyParamEndIndex(template, tagContext);
|
|
216
|
+
tagContext.endIndex = dummyEndIndex;
|
|
215
217
|
}
|
|
216
|
-
tagContexts.push(tagContext);
|
|
217
218
|
}
|
|
219
|
+
tagContexts.push(tagContext);
|
|
218
220
|
}
|
|
219
221
|
|
|
220
222
|
// できあがったTagContextの配列から、BEGEN、IFの場合は次の対応するENDが出てくるまでをsubに入れ直して構造化し、
|
|
@@ -326,7 +328,10 @@ export class SQLBuilder {
|
|
|
326
328
|
for (const value of array) {
|
|
327
329
|
// 再帰呼び出しによりposが進むので、ループのたびにposを戻す必要がある
|
|
328
330
|
pos.index = tagContext.endIndex;
|
|
329
|
-
result += this.parse(pos, template, {
|
|
331
|
+
result += this.parse(pos, template, {
|
|
332
|
+
...entity,
|
|
333
|
+
[bindName]: value
|
|
334
|
+
}, tagContext.sub, options);
|
|
330
335
|
// FORループするときは各行で改行する
|
|
331
336
|
result += '\n';
|
|
332
337
|
}
|
|
@@ -353,7 +358,7 @@ export class SQLBuilder {
|
|
|
353
358
|
case 'BIND': {
|
|
354
359
|
result += template.substring(pos.index, tagContext.startIndex);
|
|
355
360
|
pos.index = tagContext.endIndex;
|
|
356
|
-
const value =
|
|
361
|
+
const value = common.getProperty(entity, tagContext.contents);
|
|
357
362
|
switch (options?.bindType) {
|
|
358
363
|
case 'postgres': {
|
|
359
364
|
// PostgreSQL形式の場合、$Nでバインドパラメータを展開
|
|
@@ -405,7 +410,7 @@ export class SQLBuilder {
|
|
|
405
410
|
default: {
|
|
406
411
|
// generateSQLの場合
|
|
407
412
|
const escapedValue = this.extractValue(tagContext.contents, entity);
|
|
408
|
-
result +=
|
|
413
|
+
result += escapedValue ?? '';
|
|
409
414
|
}
|
|
410
415
|
}
|
|
411
416
|
break;
|
|
@@ -499,42 +504,15 @@ export class SQLBuilder {
|
|
|
499
504
|
*/
|
|
500
505
|
private evaluateCondition(condition: string, entity: Record<string, any>): boolean {
|
|
501
506
|
try {
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
return ${condition};
|
|
506
|
-
} catch(error) {
|
|
507
|
-
return false;
|
|
508
|
-
}
|
|
509
|
-
}`
|
|
510
|
-
);
|
|
511
|
-
return !!evaluate(entity);
|
|
507
|
+
const ast = new AbstractSyntaxTree();
|
|
508
|
+
const result = ast.evaluateCondition(condition, entity);
|
|
509
|
+
return result;
|
|
512
510
|
} catch (error) {
|
|
513
511
|
console.warn('Error evaluating condition:', condition, entity, error);
|
|
514
512
|
return false;
|
|
515
513
|
}
|
|
516
514
|
}
|
|
517
515
|
|
|
518
|
-
/**
|
|
519
|
-
* entityで指定したオブジェクトからドットで連結されたプロパティキーに該当する値を取得する
|
|
520
|
-
*
|
|
521
|
-
* @param {Record<string, any>} entity
|
|
522
|
-
* @param {string} property
|
|
523
|
-
* @returns {any}
|
|
524
|
-
*/
|
|
525
|
-
private getProperty(entity: Record<string, any>, property: string): any {
|
|
526
|
-
const propertyPath = property.split('.');
|
|
527
|
-
let value = entity;
|
|
528
|
-
for (const prop of propertyPath) {
|
|
529
|
-
if (value && value.hasOwnProperty(prop)) {
|
|
530
|
-
value = value[prop];
|
|
531
|
-
} else {
|
|
532
|
-
return undefined;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
return value;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
516
|
/**
|
|
539
517
|
* entityからparamで指定した値を文字列で取得する
|
|
540
518
|
* entityの値がstringの場合、SQLインジェクションの危険のある文字はエスケープする
|
|
@@ -557,7 +535,7 @@ export class SQLBuilder {
|
|
|
557
535
|
responseType?: T
|
|
558
536
|
}): ExtractValueType<T> {
|
|
559
537
|
try {
|
|
560
|
-
const value =
|
|
538
|
+
const value = common.getProperty(entity, property);
|
|
561
539
|
let result = '';
|
|
562
540
|
switch (options?.responseType) {
|
|
563
541
|
case 'array':
|