@digitalwalletcorp/sql-builder 2.0.0 → 2.1.0
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 +183 -49
- package/lib/abstract-syntax-tree.d.ts +1 -1
- package/lib/abstract-syntax-tree.js +6 -6
- package/lib/abstract-syntax-tree.js.map +1 -1
- package/lib/common.d.ts +21 -0
- package/lib/common.js +35 -4
- package/lib/common.js.map +1 -1
- package/lib/sql-builder.d.ts +21 -1
- package/lib/sql-builder.js +285 -66
- package/lib/sql-builder.js.map +1 -1
- package/package.json +3 -2
- package/src/abstract-syntax-tree.ts +5 -6
- package/src/common.ts +40 -4
- package/src/sql-builder.ts +349 -79
package/src/common.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
type PropertyResult = {
|
|
2
|
+
exists: boolean;
|
|
3
|
+
value: any;
|
|
4
|
+
}
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* entityで指定したオブジェクトからドットで連結されたプロパティキーに該当する値を取得する
|
|
3
8
|
*
|
|
@@ -6,17 +11,45 @@
|
|
|
6
11
|
* @returns {any}
|
|
7
12
|
*/
|
|
8
13
|
export function getProperty(entity: Record<string, any>, property: string): any {
|
|
14
|
+
return getPropertyResult(entity, property).value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* entityで指定したオブジェクトからドットで連結されたプロパティキーに該当する値が存在するかチェックする
|
|
19
|
+
*
|
|
20
|
+
* @param {Record<string, any>} entity
|
|
21
|
+
* @param {string} property
|
|
22
|
+
* @returns {boolean}
|
|
23
|
+
*/
|
|
24
|
+
export function hasProperty(entity: Record<string, any>, property: string): boolean {
|
|
25
|
+
return getPropertyResult(entity, property).exists;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* entityで指定したオブジェクトからドットで連結されたプロパティキーに該当する値を取得する
|
|
30
|
+
*
|
|
31
|
+
* @param {Record<string, any>} entity
|
|
32
|
+
* @param {string} property
|
|
33
|
+
* @returns {PropertyResult}
|
|
34
|
+
*/
|
|
35
|
+
export function getPropertyResult(entity: Record<string, any>, property: string): PropertyResult {
|
|
9
36
|
// `?.` または `.` でパスを分割
|
|
10
37
|
const propertyPath = property.split(/(\?\.)|\./).filter(Boolean);
|
|
11
38
|
|
|
12
39
|
// 再帰呼び出し用のヘルパー関数
|
|
13
40
|
const get = (obj: Record<string, any>, keys: string[]): any => {
|
|
14
41
|
if (keys.length === 0) {
|
|
15
|
-
return
|
|
42
|
+
return {
|
|
43
|
+
exists: true,
|
|
44
|
+
value: obj
|
|
45
|
+
} as PropertyResult;
|
|
16
46
|
}
|
|
17
47
|
// オプショナルチェイニングのチェック
|
|
18
48
|
if (obj == null) {
|
|
19
|
-
return
|
|
49
|
+
return {
|
|
50
|
+
exists: false,
|
|
51
|
+
value: undefined
|
|
52
|
+
} as PropertyResult;
|
|
20
53
|
}
|
|
21
54
|
|
|
22
55
|
const currentKey = keys[0];
|
|
@@ -27,8 +60,11 @@ export function getProperty(entity: Record<string, any>, property: string): any
|
|
|
27
60
|
return get(obj, remainingKeys);
|
|
28
61
|
}
|
|
29
62
|
// プロパティが存在しない場合は undefined を返す
|
|
30
|
-
if (!
|
|
31
|
-
return
|
|
63
|
+
if (!Object.prototype.hasOwnProperty.call(obj, currentKey)) {
|
|
64
|
+
return {
|
|
65
|
+
exists: false,
|
|
66
|
+
value: undefined
|
|
67
|
+
} as PropertyResult;
|
|
32
68
|
}
|
|
33
69
|
return get(obj[currentKey], remainingKeys);
|
|
34
70
|
};
|
package/src/sql-builder.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as common from './common';
|
|
2
2
|
import { AbstractSyntaxTree } from './abstract-syntax-tree';
|
|
3
3
|
|
|
4
|
-
type TagType = 'BEGIN' | 'IF' | '
|
|
4
|
+
type TagType = 'BEGIN' | 'IF' | 'ELSEIF' | 'ELSE' | 'FOR' | 'BIND' | 'TEXT' | 'END';
|
|
5
5
|
type ExtractValueType<T extends 'string' | 'array' | 'object'>
|
|
6
6
|
= T extends 'string'
|
|
7
7
|
? string | undefined
|
|
@@ -38,19 +38,54 @@ type BindParameterType<T extends BindType>
|
|
|
38
38
|
: T extends 'mssql' ? Record<string, any>
|
|
39
39
|
: undefined;
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
match: string;
|
|
44
|
-
contents: string;
|
|
41
|
+
// すべてのタグの基底
|
|
42
|
+
interface BaseTagContext {
|
|
45
43
|
startIndex: number;
|
|
46
44
|
endIndex: number;
|
|
45
|
+
parent: ParentTagContext | null;
|
|
46
|
+
match: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 子要素を持つ「親」になれるタグの共通定義
|
|
50
|
+
interface ParentTagContext extends BaseTagContext {
|
|
51
|
+
type: 'BEGIN' | 'IF' | 'ELSEIF' | 'ELSE' | 'FOR';
|
|
52
|
+
contents: string;
|
|
47
53
|
sub: TagContext[];
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
conditionMatched: boolean; // この要素自体の条件が成立したか
|
|
55
|
+
childConditionMatched: boolean; // 子要素のいずれかの条件が成立したか(BEGIN/IF用)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface ContainerTagContext extends ParentTagContext {
|
|
59
|
+
type: 'BEGIN' | 'FOR';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface BranchTagContext extends ParentTagContext {
|
|
63
|
+
type: 'IF' | 'ELSEIF' | 'ELSE';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface BindTagContext extends BaseTagContext {
|
|
67
|
+
type: 'BIND';
|
|
68
|
+
contents: string;
|
|
69
|
+
isPgArray?: boolean;
|
|
70
|
+
pgArrayCast?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface TextTagContext extends BaseTagContext {
|
|
74
|
+
type: 'TEXT';
|
|
75
|
+
contents: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface EndTagContext extends BaseTagContext {
|
|
79
|
+
type: 'END';
|
|
52
80
|
}
|
|
53
81
|
|
|
82
|
+
type TagContext
|
|
83
|
+
= ContainerTagContext
|
|
84
|
+
| BranchTagContext
|
|
85
|
+
| TextTagContext
|
|
86
|
+
| BindTagContext
|
|
87
|
+
| EndTagContext;
|
|
88
|
+
|
|
54
89
|
interface SharedIndex {
|
|
55
90
|
index: number;
|
|
56
91
|
}
|
|
@@ -195,45 +230,100 @@ export class SQLBuilder {
|
|
|
195
230
|
const matches = template.matchAll(this.REGEX_TAG_PATTERN);
|
|
196
231
|
|
|
197
232
|
// まず最初にREGEX_TAG_PATTERNで解析した情報をそのままフラットにTagContextの配列に格納
|
|
198
|
-
let
|
|
233
|
+
let lastIndex = 0;
|
|
199
234
|
const tagContexts: TagContext[] = [];
|
|
200
235
|
for (const match of matches) {
|
|
201
236
|
const matchContent = match[0];
|
|
202
237
|
const index = match.index;
|
|
203
|
-
|
|
204
|
-
|
|
238
|
+
|
|
239
|
+
if (lastIndex < index) {
|
|
240
|
+
tagContexts.push({
|
|
241
|
+
type: 'TEXT',
|
|
242
|
+
match: '',
|
|
243
|
+
contents: template.substring(lastIndex, index),
|
|
244
|
+
startIndex: lastIndex,
|
|
245
|
+
endIndex: index,
|
|
246
|
+
parent: null
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let tagContext: TagContext = {
|
|
205
251
|
type: 'BIND', // ダミーの初期値。後続処理で適切なタイプに変更する。
|
|
206
|
-
match: matchContent,
|
|
207
252
|
contents: '',
|
|
208
253
|
startIndex: index,
|
|
209
254
|
endIndex: index + matchContent.length,
|
|
210
|
-
sub: [],
|
|
211
255
|
parent: null,
|
|
212
|
-
|
|
256
|
+
match: matchContent
|
|
213
257
|
};
|
|
214
258
|
switch (true) {
|
|
215
259
|
case matchContent === '/*BEGIN*/': {
|
|
216
|
-
tagContext
|
|
260
|
+
tagContext = {
|
|
261
|
+
...tagContext,
|
|
262
|
+
type: 'BEGIN',
|
|
263
|
+
sub: [],
|
|
264
|
+
conditionMatched: false,
|
|
265
|
+
childConditionMatched: false
|
|
266
|
+
} as ContainerTagContext;
|
|
217
267
|
break;
|
|
218
268
|
}
|
|
219
269
|
case matchContent.startsWith('/*IF'): {
|
|
220
|
-
tagContext
|
|
270
|
+
tagContext = {
|
|
271
|
+
...tagContext,
|
|
272
|
+
type: 'IF',
|
|
273
|
+
sub: [],
|
|
274
|
+
conditionMatched: false,
|
|
275
|
+
childConditionMatched: false
|
|
276
|
+
} as BranchTagContext;
|
|
221
277
|
const contentMatcher = matchContent.match(/^\/\*IF\s+(.*?)\*\/$/);
|
|
222
278
|
tagContext.contents = contentMatcher && contentMatcher[1] || '';
|
|
223
279
|
break;
|
|
224
280
|
}
|
|
281
|
+
case matchContent.startsWith('/*ELSEIF'): {
|
|
282
|
+
tagContext = {
|
|
283
|
+
...tagContext,
|
|
284
|
+
type: 'ELSEIF',
|
|
285
|
+
sub: [],
|
|
286
|
+
conditionMatched: false,
|
|
287
|
+
childConditionMatched: false
|
|
288
|
+
} as BranchTagContext;
|
|
289
|
+
const contentMatcher = matchContent.match(/^\/\*ELSEIF\s+(.*?)\*\/$/);
|
|
290
|
+
tagContext.contents = contentMatcher && contentMatcher[1] || '';
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
case matchContent.startsWith('/*ELSE*/'): {
|
|
294
|
+
tagContext = {
|
|
295
|
+
...tagContext,
|
|
296
|
+
type: 'ELSE',
|
|
297
|
+
sub: [],
|
|
298
|
+
conditionMatched: false,
|
|
299
|
+
childConditionMatched: false
|
|
300
|
+
} as BranchTagContext;
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
225
303
|
case matchContent.startsWith('/*FOR'): {
|
|
226
|
-
tagContext
|
|
304
|
+
tagContext = {
|
|
305
|
+
...tagContext,
|
|
306
|
+
type: 'FOR',
|
|
307
|
+
sub: [],
|
|
308
|
+
conditionMatched: false,
|
|
309
|
+
childConditionMatched: false
|
|
310
|
+
} as ContainerTagContext;
|
|
227
311
|
const contentMatcher = matchContent.match(/^\/\*FOR\s+(.*?)\*\/$/);
|
|
228
312
|
tagContext.contents = contentMatcher && contentMatcher[1] || '';
|
|
229
313
|
break;
|
|
230
314
|
}
|
|
231
315
|
case matchContent === '/*END*/': {
|
|
232
|
-
tagContext
|
|
316
|
+
tagContext = {
|
|
317
|
+
...tagContext,
|
|
318
|
+
type: 'END'
|
|
319
|
+
} as EndTagContext;
|
|
233
320
|
break;
|
|
234
321
|
}
|
|
235
322
|
default: {
|
|
236
|
-
tagContext
|
|
323
|
+
tagContext = {
|
|
324
|
+
...tagContext,
|
|
325
|
+
type: 'BIND'
|
|
326
|
+
} as BindTagContext;
|
|
237
327
|
const contentMatcher = matchContent.match(/\/\*(.*?)\*\//);
|
|
238
328
|
tagContext.contents = contentMatcher && contentMatcher[1]?.trim() || '';
|
|
239
329
|
// ダミー値の終了位置をendIndexに設定
|
|
@@ -253,26 +343,59 @@ export class SQLBuilder {
|
|
|
253
343
|
}
|
|
254
344
|
}
|
|
255
345
|
}
|
|
346
|
+
|
|
256
347
|
tagContexts.push(tagContext);
|
|
348
|
+
|
|
349
|
+
lastIndex = tagContext.endIndex;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ループを抜けた後に残った部分をTEXTタグとして追加
|
|
353
|
+
if (lastIndex < template.length) {
|
|
354
|
+
tagContexts.push({
|
|
355
|
+
type: 'TEXT',
|
|
356
|
+
match: '',
|
|
357
|
+
contents: template.substring(lastIndex),
|
|
358
|
+
startIndex: lastIndex,
|
|
359
|
+
endIndex: template.length,
|
|
360
|
+
parent: null
|
|
361
|
+
});
|
|
257
362
|
}
|
|
258
363
|
|
|
259
|
-
// できあがったTagContextの配列から、
|
|
260
|
-
//
|
|
364
|
+
// できあがったTagContextの配列から、
|
|
365
|
+
// BEGENの場合は対応するENDが出てくるまで、
|
|
366
|
+
// IFの場合は次の対応するENDが出てくるまでをsubに入れ直して構造化し、
|
|
367
|
+
// 以下のような構造に変更する
|
|
261
368
|
/**
|
|
262
369
|
* ```
|
|
263
370
|
* BEGIN
|
|
264
371
|
* ├ IF
|
|
265
|
-
* ├ BIND
|
|
266
|
-
* ├
|
|
372
|
+
* ├ BIND(無いこともある)
|
|
373
|
+
* ├ ELSEIF
|
|
374
|
+
* ├ BIND(無いこともある)
|
|
375
|
+
* ├ ELSE
|
|
376
|
+
* ├ BIND(無いこともある)
|
|
267
377
|
* ├ END
|
|
268
378
|
* ├ BIND
|
|
269
379
|
* END
|
|
270
380
|
* ```
|
|
271
381
|
*/
|
|
272
|
-
const parentTagContexts:
|
|
382
|
+
const parentTagContexts: ParentTagContext[] = [];
|
|
273
383
|
const newTagContexts: TagContext[] = [];
|
|
274
384
|
for (const tagContext of tagContexts) {
|
|
275
385
|
switch (tagContext.type) {
|
|
386
|
+
case 'TEXT':
|
|
387
|
+
case 'BIND': {
|
|
388
|
+
const parentTagContext = parentTagContexts[parentTagContexts.length - 1];
|
|
389
|
+
if (parentTagContext) {
|
|
390
|
+
// 親タグがある
|
|
391
|
+
tagContext.parent = parentTagContext;
|
|
392
|
+
parentTagContext.sub.push(tagContext);
|
|
393
|
+
} else {
|
|
394
|
+
// 親タグがない(最上位)
|
|
395
|
+
newTagContexts.push(tagContext);
|
|
396
|
+
}
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
276
399
|
case 'BEGIN':
|
|
277
400
|
case 'IF':
|
|
278
401
|
case 'FOR': {
|
|
@@ -289,23 +412,42 @@ export class SQLBuilder {
|
|
|
289
412
|
parentTagContexts.push(tagContext);
|
|
290
413
|
break;
|
|
291
414
|
}
|
|
415
|
+
case 'ELSEIF':
|
|
416
|
+
case 'ELSE': {
|
|
417
|
+
const ifTagContext = this.findParentTagContext(parentTagContexts[parentTagContexts.length - 1], 'IF');
|
|
418
|
+
if (ifTagContext) {
|
|
419
|
+
// 親タグ(IF)がある
|
|
420
|
+
tagContext.parent = ifTagContext;
|
|
421
|
+
ifTagContext.sub.push(tagContext);
|
|
422
|
+
} else {
|
|
423
|
+
throw new Error(`[SQLBuilder] ${tagContext.type} must be inside IF.`);
|
|
424
|
+
}
|
|
425
|
+
// 後続処理で自身が親になるので自身を追加
|
|
426
|
+
parentTagContexts.push(tagContext); // これは暫定的なものなのでENDで削除する必要がある
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
292
429
|
case 'END': {
|
|
293
|
-
|
|
294
|
-
|
|
430
|
+
let parentTagContext = parentTagContexts[parentTagContexts.length - 1];
|
|
431
|
+
if (!parentTagContext) {
|
|
432
|
+
throw new Error(`[SQLBuilder] 'END' tag without corresponding parent.`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (parentTagContext.type === 'ELSEIF' || parentTagContext.type === 'ELSE') {
|
|
436
|
+
// 暫定追加されたELSEIF/ELSEを除去
|
|
437
|
+
while(parentTagContexts.length && ['ELSEIF', 'ELSE'].includes(parentTagContexts[parentTagContexts.length - 1].type)) {
|
|
438
|
+
parentTagContexts.pop();
|
|
439
|
+
}
|
|
440
|
+
parentTagContext = parentTagContexts[parentTagContexts.length - 1];
|
|
441
|
+
}
|
|
295
442
|
tagContext.parent = parentTagContext;
|
|
296
443
|
parentTagContext.sub.push(tagContext);
|
|
444
|
+
|
|
445
|
+
parentTagContexts.pop();
|
|
297
446
|
break;
|
|
298
447
|
}
|
|
299
448
|
default: {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
// 親タグがある
|
|
303
|
-
tagContext.parent = parentTagContext;
|
|
304
|
-
parentTagContext.sub.push(tagContext);
|
|
305
|
-
} else {
|
|
306
|
-
// 親タグがない(最上位)
|
|
307
|
-
newTagContexts.push(tagContext);
|
|
308
|
-
}
|
|
449
|
+
// 型定義にない型という扱いになるのでanyキャストする(到達不可能?)
|
|
450
|
+
throw new Error(`[SQLBuilder] Unknown TagType '${(tagContext as any).type}`);
|
|
309
451
|
}
|
|
310
452
|
}
|
|
311
453
|
}
|
|
@@ -319,14 +461,14 @@ export class SQLBuilder {
|
|
|
319
461
|
* @param {SharedIndex} pos 現在処理している文字列の先頭インデックス
|
|
320
462
|
* @param {string} template
|
|
321
463
|
* @param {Record<string, any>} entity
|
|
322
|
-
* @param {TagContext[]} tagContexts
|
|
464
|
+
* @param {(TagContext | ParentTagContext)[]} tagContexts
|
|
323
465
|
* @param {*} [options]
|
|
324
466
|
* ├ bindType BindType
|
|
325
467
|
* ├ bindIndex number
|
|
326
468
|
* ├ bindParams BindParameterType<T>
|
|
327
469
|
* @returns {string}
|
|
328
470
|
*/
|
|
329
|
-
private parse<T extends BindType>(pos: SharedIndex, template: string, entity: Record<string, any>, tagContexts: TagContext[], options?: {
|
|
471
|
+
private parse<T extends BindType>(pos: SharedIndex, template: string, entity: Record<string, any>, tagContexts: (TagContext | ParentTagContext)[], options?: {
|
|
330
472
|
bindType: T,
|
|
331
473
|
bindIndex: number,
|
|
332
474
|
bindParams: BindParameterType<T>
|
|
@@ -334,6 +476,14 @@ export class SQLBuilder {
|
|
|
334
476
|
let result = '';
|
|
335
477
|
for (const tagContext of tagContexts) {
|
|
336
478
|
switch (tagContext.type) {
|
|
479
|
+
case 'TEXT': {
|
|
480
|
+
// テキストは無条件に書き出し
|
|
481
|
+
// 改行だけなど、trimすると空になる文字列もあるが、
|
|
482
|
+
// どの部分がスキップされたのかわかるようにあえて残すことにする
|
|
483
|
+
result += tagContext.contents;
|
|
484
|
+
pos.index = tagContext.endIndex;
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
337
487
|
case 'BEGIN': {
|
|
338
488
|
result += template.substring(pos.index, tagContext.startIndex);
|
|
339
489
|
pos.index = tagContext.endIndex;
|
|
@@ -341,45 +491,92 @@ export class SQLBuilder {
|
|
|
341
491
|
const beginBlockResult = this.parse(pos, template, entity, tagContext.sub, options);
|
|
342
492
|
// BEGIN内のIF、FORのいずれかで成立したものがあった場合は結果を出力
|
|
343
493
|
if (tagContext.sub.some(sub =>
|
|
344
|
-
(sub.type === 'IF' || sub.type === 'FOR') && sub.
|
|
494
|
+
(sub.type === 'IF' || sub.type === 'FOR') && sub.conditionMatched
|
|
345
495
|
)) {
|
|
346
496
|
result += beginBlockResult;
|
|
347
497
|
}
|
|
348
498
|
break;
|
|
349
499
|
}
|
|
350
500
|
case 'IF': {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
result += this.parse(pos, template, entity,
|
|
358
|
-
|
|
359
|
-
// IF
|
|
501
|
+
const conditionMatched = this.evaluateCondition(tagContext.contents, entity);
|
|
502
|
+
if (conditionMatched) {
|
|
503
|
+
tagContext.conditionMatched = true; // 自身の評価結果
|
|
504
|
+
pos.index = tagContext.endIndex;
|
|
505
|
+
// 条件が成立したので、自身のsubにある条件タグを排除する
|
|
506
|
+
const children = tagContext.sub.filter(a => a.type !== 'ELSEIF' && a.type !== 'ELSE');
|
|
507
|
+
result += this.parse(pos, template, entity, children, options);
|
|
508
|
+
|
|
509
|
+
// IFタグからENDを探すときは、自身のsubの最後の要素
|
|
360
510
|
const endTagContext = tagContext.sub[tagContext.sub.length - 1];
|
|
361
511
|
pos.index = endTagContext.endIndex;
|
|
512
|
+
} else {
|
|
513
|
+
// 条件不成立
|
|
514
|
+
const nextBranch = tagContext.sub.find(a => a.type === 'ELSEIF' || a.type === 'ELSE');
|
|
515
|
+
if (nextBranch) {
|
|
516
|
+
pos.index = nextBranch.startIndex;
|
|
517
|
+
result += this.parse(pos, template, entity, [nextBranch], options);
|
|
518
|
+
} else {
|
|
519
|
+
// 次の条件がない場合はENDの後ろまでポインタを飛ばす
|
|
520
|
+
const endTagContext = tagContext.sub[tagContext.sub.length - 1];
|
|
521
|
+
pos.index = endTagContext.endIndex;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
case 'ELSEIF':
|
|
527
|
+
case 'ELSE': {
|
|
528
|
+
const conditionMatched = tagContext.type === 'ELSE' || this.evaluateCondition(tagContext.contents, entity);
|
|
529
|
+
if (conditionMatched) {
|
|
530
|
+
tagContext.conditionMatched = true; // 自身の評価結果
|
|
531
|
+
pos.index = tagContext.endIndex;
|
|
532
|
+
// 条件が成立したので、自身のsubにある条件タグを排除する
|
|
533
|
+
const children = tagContext.sub.filter(a => a.type !== 'ELSEIF' && a.type !== 'ELSE');
|
|
534
|
+
result += this.parse(pos, template, entity, children, options);
|
|
535
|
+
|
|
536
|
+
// ELSEIF/ELSEからIFを探すときは自身の親そのもの
|
|
537
|
+
const ifTagContext = tagContext.parent!;
|
|
538
|
+
ifTagContext.childConditionMatched = true; // 親が持っている評価結果にtrueを設定
|
|
539
|
+
|
|
540
|
+
// ELSEIF/ELSEからENDを探すときは、自身の兄弟の最後の要素
|
|
541
|
+
const endTagContext = this.seekSiblingTagContext(tagContext, 'END', 'next')!;
|
|
542
|
+
pos.index = endTagContext.endIndex;
|
|
543
|
+
} else {
|
|
544
|
+
// 条件不成立
|
|
545
|
+
const nextBranch = this.seekSiblingTagContext(tagContext, ['ELSEIF', 'ELSE'], 'next');
|
|
546
|
+
if (nextBranch) {
|
|
547
|
+
pos.index = nextBranch.startIndex;
|
|
548
|
+
result += this.parse(pos, template, entity, [nextBranch], options);
|
|
549
|
+
} else {
|
|
550
|
+
// 次の条件がない場合はENDの後ろまでポインタを飛ばす
|
|
551
|
+
const endTagContext = tagContext.sub[tagContext.sub.length - 1];
|
|
552
|
+
pos.index = endTagContext.endIndex;
|
|
553
|
+
}
|
|
362
554
|
}
|
|
363
555
|
break;
|
|
364
556
|
}
|
|
365
557
|
case 'FOR': {
|
|
366
|
-
result += template.substring(pos.index, tagContext.startIndex);
|
|
367
|
-
pos.index = tagContext.endIndex;
|
|
368
558
|
const [bindName, collectionName] = tagContext.contents.split(':').map(a => a.trim());
|
|
369
559
|
const array = common.getProperty(entity, collectionName);
|
|
370
560
|
if (Array.isArray(array) && array.length) {
|
|
371
|
-
tagContext.
|
|
561
|
+
tagContext.conditionMatched = true;
|
|
372
562
|
for (const value of array) {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
563
|
+
const children = tagContext.sub.filter(a => a.type !== 'END');
|
|
564
|
+
result += this.parse(
|
|
565
|
+
pos,
|
|
566
|
+
template,
|
|
567
|
+
{
|
|
568
|
+
...entity,
|
|
569
|
+
[bindName]: value
|
|
570
|
+
},
|
|
571
|
+
children,
|
|
572
|
+
options
|
|
573
|
+
);
|
|
381
574
|
result += '\n';
|
|
382
575
|
}
|
|
576
|
+
|
|
577
|
+
// FORのENDまでポインタを進める
|
|
578
|
+
const endTag = tagContext.sub[tagContext.sub.length - 1];
|
|
579
|
+
pos.index = endTag.endIndex;
|
|
383
580
|
} else {
|
|
384
581
|
// FORブロックを丸ごとスキップ
|
|
385
582
|
const endTagContext = tagContext.sub[tagContext.sub.length - 1];
|
|
@@ -388,20 +585,26 @@ export class SQLBuilder {
|
|
|
388
585
|
break;
|
|
389
586
|
}
|
|
390
587
|
case 'END': {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
//
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
588
|
+
const parent = tagContext.parent;
|
|
589
|
+
if (parent) {
|
|
590
|
+
switch (true) {
|
|
591
|
+
// BEGIN/IF/FORに対応するENDは、それぞれの処理で考慮すると複雑化するので
|
|
592
|
+
// すべてENDタグでどういう状態になったのは判定して動きを制御する
|
|
593
|
+
case parent.type === 'BEGIN'
|
|
594
|
+
&& parent.sub.some(a => a.type === 'IF' || a.type === 'FOR')
|
|
595
|
+
&& !parent.sub.some(a => a.type === 'IF' || a.type === 'FOR' && a.conditionMatched):
|
|
596
|
+
// BEGINに対応するENDの場合
|
|
597
|
+
// ・子要素にIFまたはFORが存在する
|
|
598
|
+
// ・子要素のIFまたはFORにstatus=10(成功)を示すものが1つもない
|
|
599
|
+
case parent.type === 'IF' && !parent.conditionMatched:
|
|
600
|
+
// IFに対応するENDの場合、IFのstatusがstatus=10(成功)になっていない
|
|
601
|
+
case parent.type === 'IF' && parent.sub.some(a => a.type === 'ELSEIF' || a.type === 'ELSE'):
|
|
602
|
+
case parent.type === 'FOR' && !parent.conditionMatched:
|
|
603
|
+
// FORに対応するENDの場合、FORのstatusがstatus=10(成功)になっていない
|
|
604
|
+
pos.index = tagContext.endIndex;
|
|
605
|
+
return result;
|
|
606
|
+
default:
|
|
607
|
+
}
|
|
405
608
|
}
|
|
406
609
|
result += template.substring(pos.index, tagContext.startIndex);
|
|
407
610
|
pos.index = tagContext.endIndex;
|
|
@@ -410,14 +613,13 @@ export class SQLBuilder {
|
|
|
410
613
|
case 'BIND': {
|
|
411
614
|
result += template.substring(pos.index, tagContext.startIndex);
|
|
412
615
|
pos.index = tagContext.endIndex;
|
|
616
|
+
const propertyResult = common.getPropertyResult(entity, tagContext.contents);
|
|
413
617
|
// ★ UNKNOWN_TAG 判定
|
|
414
|
-
|
|
415
|
-
if (!hasProperty) {
|
|
618
|
+
if (!propertyResult.exists) {
|
|
416
619
|
// UNKNOWN_TAG → エラーを発行
|
|
417
|
-
throw new Error(`[SQLBuilder] The property
|
|
620
|
+
throw new Error(`[SQLBuilder] The property '${tagContext.contents}' is not found in the bind entity. (Template index: ${tagContext.startIndex})`);
|
|
418
621
|
}
|
|
419
|
-
const
|
|
420
|
-
const value = rawValue === undefined ? null : rawValue;
|
|
622
|
+
const value = propertyResult.value === undefined ? null : propertyResult.value;
|
|
421
623
|
switch (options?.bindType) {
|
|
422
624
|
case 'postgres': {
|
|
423
625
|
// PostgreSQL形式の場合、$Nでバインドパラメータを展開
|
|
@@ -510,11 +712,75 @@ export class SQLBuilder {
|
|
|
510
712
|
}
|
|
511
713
|
}
|
|
512
714
|
|
|
715
|
+
if (tagContexts.length && tagContexts[0].parent) {
|
|
716
|
+
// IFタグ解析中のELSEIF/ELSEなどが残っている場合は以降の文字列を連結せず、IFの解析結果のみ返す
|
|
717
|
+
return result;
|
|
718
|
+
}
|
|
719
|
+
|
|
513
720
|
// 最後に余った部分を追加する
|
|
514
721
|
result += template.substring(pos.index);
|
|
515
722
|
return result;
|
|
516
723
|
}
|
|
517
724
|
|
|
725
|
+
/**
|
|
726
|
+
* 指定したタグから親を遡り、指定したタグタイプのタグコンテキストを返す
|
|
727
|
+
* 見つからない場合はundefinedを返す
|
|
728
|
+
*
|
|
729
|
+
* @param {TagContext | ParentTagContext | null} tagContext
|
|
730
|
+
* @param {T} tagType
|
|
731
|
+
* @returns {(TagContext & { type: T }) | undefined}
|
|
732
|
+
*/
|
|
733
|
+
private findParentTagContext<T extends TagType>(tagContext: TagContext | ParentTagContext | null, tagType: T): (TagContext & { type: T }) | undefined {
|
|
734
|
+
let targetTagContext = tagContext;
|
|
735
|
+
while (targetTagContext != null) {
|
|
736
|
+
if (targetTagContext.type === tagType) {
|
|
737
|
+
return targetTagContext as Extract<TagContext, { type: T }>;
|
|
738
|
+
}
|
|
739
|
+
targetTagContext = targetTagContext.parent;
|
|
740
|
+
}
|
|
741
|
+
return undefined;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* 指定したタグの兄弟をたどり、指定したタグタイプのタグコンテキストを返す
|
|
746
|
+
* 見つからない場合はundefinedを返す
|
|
747
|
+
* ユースケースとしては、ELSEIF/ELSEから同じIFに属するENDを探す
|
|
748
|
+
*
|
|
749
|
+
* @param {TagContext} tagContext
|
|
750
|
+
* @param {TagType | TagType[]} tagType
|
|
751
|
+
* @param {'previous' | 'next'} direction
|
|
752
|
+
* @returns {TagContext | undefined}
|
|
753
|
+
*/
|
|
754
|
+
private seekSiblingTagContext(tagContext: TagContext, tagType: TagType | TagType[], direction: 'previous' | 'next' = 'next'): TagContext | undefined {
|
|
755
|
+
const parent = tagContext.parent;
|
|
756
|
+
if (parent) {
|
|
757
|
+
// 自身が所属するインデックスを取得
|
|
758
|
+
const startIndex = parent.sub.indexOf(tagContext);
|
|
759
|
+
if (startIndex < 0) {
|
|
760
|
+
return undefined;
|
|
761
|
+
}
|
|
762
|
+
const tagTypes = Array.isArray(tagType) ? tagType : [tagType];
|
|
763
|
+
switch (direction) {
|
|
764
|
+
case 'previous':
|
|
765
|
+
for (let i = startIndex - 1; 0 <= i; i--) {
|
|
766
|
+
if (tagTypes.some(a => a === parent.sub[i].type)) {
|
|
767
|
+
return parent.sub[i];
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
break;
|
|
771
|
+
case 'next':
|
|
772
|
+
for (let i = startIndex + 1; i < parent.sub.length; i++) {
|
|
773
|
+
if (tagTypes.some(a => a === parent.sub[i].type)) {
|
|
774
|
+
return parent.sub[i];
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
break;
|
|
778
|
+
default:
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return undefined;
|
|
782
|
+
}
|
|
783
|
+
|
|
518
784
|
/**
|
|
519
785
|
* ダミーパラメータの終了インデックスを返す
|
|
520
786
|
*
|
|
@@ -637,7 +903,11 @@ export class SQLBuilder {
|
|
|
637
903
|
private extractValue<T extends 'string' | 'array' | 'object' = 'string'>(property: string, entity: Record<string, any>, options?: {
|
|
638
904
|
responseType?: T
|
|
639
905
|
}): ExtractValueType<T> {
|
|
640
|
-
const
|
|
906
|
+
const propertyResult = common.getPropertyResult(entity, property);
|
|
907
|
+
if (!propertyResult.exists) {
|
|
908
|
+
throw new Error(`[SQLBuilder] The property '${property}' is not found in the entity: ${JSON.stringify(entity)}`);
|
|
909
|
+
}
|
|
910
|
+
const value = propertyResult.value;
|
|
641
911
|
if (value == null) {
|
|
642
912
|
return 'NULL' as ExtractValueType<T>;
|
|
643
913
|
}
|