@digitalwalletcorp/sql-builder 1.0.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/LICENSE +21 -0
- package/README.md +187 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +18 -0
- package/lib/index.js.map +1 -0
- package/lib/sql-builder.d.ts +103 -0
- package/lib/sql-builder.js +416 -0
- package/lib/sql-builder.js.map +1 -0
- package/package.json +42 -0
- package/src/index.ts +1 -0
- package/src/sql-builder.ts +441 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 digitalwalletcorp
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# SQL Builder
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@digitalwalletcorp/sql-builder) [](https://opensource.org/licenses/MIT) [](https://github.com/digitalwalletcorp/sql-builder/actions) [](https://codecov.io/gh/digitalwalletcorp/sql-builder)
|
|
4
|
+
|
|
5
|
+
Inspired by Java's S2Dao, this TypeScript/JavaScript library dynamically generates SQL. It embeds entity objects into SQL templates, simplifying complex query construction and enhancing readability. Ideal for flexible, type-safe SQL generation without a full ORM. It efficiently handles dynamic `WHERE` clauses, parameter binding, and looping, reducing boilerplate code.
|
|
6
|
+
|
|
7
|
+
The core mechanism involves parsing special SQL comments (`/*IF ...*/`, `/*BEGIN...*/`, etc.) in a template and generating a final query based on a provided data object.
|
|
8
|
+
|
|
9
|
+
## ✨ Features
|
|
10
|
+
|
|
11
|
+
* Dynamic Query Generation: Build complex SQL queries dynamically at runtime.
|
|
12
|
+
* Conditional Logic (`/*IF...*/`): Automatically include or exclude SQL fragments based on JavaScript conditions evaluated against your data.
|
|
13
|
+
* Optional Blocks (`/*BEGIN...*/`): Wrap entire clauses (like `WHERE`) that are only included if at least one inner `/*IF...*/` condition is met.
|
|
14
|
+
* Looping (`/*FOR...*/`): Generate repetitive SQL snippets by iterating over arrays in your data (e.g., for multiple `LIKE` or `OR` conditions).
|
|
15
|
+
* Simple Parameter Binding: Easily bind values from your data object into the SQL query.
|
|
16
|
+
* Zero Dependencies: A single, lightweight class with no external library requirements.
|
|
17
|
+
|
|
18
|
+
## ✅ Compatibility
|
|
19
|
+
|
|
20
|
+
This library is written in pure, environment-agnostic JavaScript/TypeScript and has zero external dependencies, allowing it to run in various environments.
|
|
21
|
+
|
|
22
|
+
- ✅ **Node.js**: Designed and optimized for server-side use in any modern Node.js environment. This is the **primary and recommended** use case.
|
|
23
|
+
- ⚠️ **Browser-like Environments (Advanced)**: While technically capable of running in browsers (e.g., for use with in-browser databases like SQLite via WebAssembly), generating SQL on the client-side to be sent to a server **is a significant security risk and is strongly discouraged** in typical web applications.
|
|
24
|
+
|
|
25
|
+
## 📦 Instllation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @digitalwalletcorp/sql-builder
|
|
29
|
+
# or
|
|
30
|
+
yarn add @digitalwalletcorp/sql-builder
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 📖 How It Works & Usage
|
|
34
|
+
|
|
35
|
+
You provide the `SQLBuilder` with a template string containing special, S2Dao-style comments and a data object (the "bind entity"). The builder parses the template and generates the final SQL.
|
|
36
|
+
|
|
37
|
+
##### Example 1: Dynamic WHERE Clause
|
|
38
|
+
|
|
39
|
+
This is the most common use case. The `WHERE` clause is built dynamically based on which properties exist in the `bindEntity`.
|
|
40
|
+
|
|
41
|
+
##### Template:
|
|
42
|
+
|
|
43
|
+
```sql
|
|
44
|
+
SELECT
|
|
45
|
+
id,
|
|
46
|
+
project_name,
|
|
47
|
+
status
|
|
48
|
+
FROM activity
|
|
49
|
+
/*BEGIN*/WHERE
|
|
50
|
+
1 = 1
|
|
51
|
+
/*IF projectNames != null && projectNames.length*/AND project_name IN /*projectNames*/('project1')/*END*/
|
|
52
|
+
/*IF statuses != null && statuses.length*/AND status IN /*statuses*/(1)/*END*/
|
|
53
|
+
/*END*/
|
|
54
|
+
ORDER BY started_at DESC
|
|
55
|
+
LIMIT /*limit*/100
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
##### Code:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { SQLBuilder } from '@digitalwalletcorp/sql-builder';
|
|
62
|
+
|
|
63
|
+
const builder = new SQLBuilder();
|
|
64
|
+
|
|
65
|
+
const template = `...`; // The SQL template from above
|
|
66
|
+
|
|
67
|
+
// SCENARIO A: Only `statuses` and `limit` are provided.
|
|
68
|
+
const bindEntity1 = {
|
|
69
|
+
statuses: [1, 2, 5],
|
|
70
|
+
limit: 50
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const sql1 = builder.generateSQL(template, bindEntity1);
|
|
74
|
+
console.log(sql1);
|
|
75
|
+
|
|
76
|
+
// SCENARIO B: No filter conditions are met, so the entire WHERE clause is removed.
|
|
77
|
+
const bindEntity2 = {
|
|
78
|
+
limit: 100
|
|
79
|
+
};
|
|
80
|
+
const sql2 = builder.generateSQL(template, bindEntity2);
|
|
81
|
+
console.log(sql2);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
##### Resulting SQL:
|
|
85
|
+
|
|
86
|
+
* SQL 1 (Scenario A): The `project_name` condition is excluded, but the `status` condition is included.
|
|
87
|
+
|
|
88
|
+
```sql
|
|
89
|
+
SELECT
|
|
90
|
+
id,
|
|
91
|
+
project_name,
|
|
92
|
+
status
|
|
93
|
+
FROM activity
|
|
94
|
+
WHERE
|
|
95
|
+
1 = 1
|
|
96
|
+
AND status IN (1,2,5)
|
|
97
|
+
ORDER BY started_at DESC
|
|
98
|
+
LIMIT 50
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
* SQL 2 (Scenario B): Because no `/*IF...*/` conditions inside the `/*BEGIN*/.../*END*/` block were met, the entire block (including the `WHERE` keyword) is omitted.
|
|
102
|
+
|
|
103
|
+
```sql
|
|
104
|
+
SELECT
|
|
105
|
+
id,
|
|
106
|
+
project_name,
|
|
107
|
+
status
|
|
108
|
+
FROM activity
|
|
109
|
+
ORDER BY started_at DESC
|
|
110
|
+
LIMIT 100
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
##### Example 2: FOR Loop
|
|
114
|
+
|
|
115
|
+
Use a `/*FOR...*/` block to iterate over an array and generate SQL for each item. This is useful for building multiple `LIKE` conditions.
|
|
116
|
+
|
|
117
|
+
##### Template:
|
|
118
|
+
|
|
119
|
+
```sql
|
|
120
|
+
SELECT * FROM activity
|
|
121
|
+
WHERE 1 = 1
|
|
122
|
+
/*FOR name:projectNames*/AND project_name LIKE '%' || /*name*/'default' || '%'/*END*/
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
##### Code:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import { SQLBuilder } from '@digitalwalletcorp/sql-builder';
|
|
129
|
+
|
|
130
|
+
const builder = new SQLBuilder();
|
|
131
|
+
const template = `...`; // The SQL template from above
|
|
132
|
+
|
|
133
|
+
const bindEntity = {
|
|
134
|
+
projectNames: ['api', 'batch', 'frontend']
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const sql = builder.generateSQL(template, bindEntity);
|
|
138
|
+
console.log(sql);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
##### Resulting SQL:
|
|
142
|
+
|
|
143
|
+
```sql
|
|
144
|
+
SELECT * FROM activity
|
|
145
|
+
WHERE 1 = 1
|
|
146
|
+
AND project_name LIKE '%' || 'api' || '%'
|
|
147
|
+
AND project_name LIKE '%' || 'batch' || '%'
|
|
148
|
+
AND project_name LIKE '%' || 'frontend' || '%'
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## 📚 API Reference
|
|
152
|
+
|
|
153
|
+
##### `new SQLBuilder()`
|
|
154
|
+
|
|
155
|
+
Creates a new instance of the SQL builder.
|
|
156
|
+
|
|
157
|
+
##### `generateSQL(template: string, entity: Record<string, any>): string`
|
|
158
|
+
|
|
159
|
+
Generates a final SQL string by processing the template with the provided data entity.
|
|
160
|
+
|
|
161
|
+
* `template`: The SQL template string containing S2Dao-style comments.
|
|
162
|
+
* `entity`: A data object whose properties are used for evaluating conditions (`/*IF...*/`) and binding values (`/*variable*/`).
|
|
163
|
+
* Returns: The generated SQL string.
|
|
164
|
+
|
|
165
|
+
## 🪄 Special Comment Syntax
|
|
166
|
+
|
|
167
|
+
| Tag | Syntax | Description |
|
|
168
|
+
| --- | --- | --- |
|
|
169
|
+
| IF | `/*IF condition*/ ... /*END*/` | Includes the enclosed SQL fragment only if the `condition` evaluates to a truthy value. The condition is a JavaScript expression evaluated against the `entity` object. |
|
|
170
|
+
| BEGIN | `/*BEGIN*/ ... /*END*/` | A wrapper block, typically for a `WHERE` clause. The entire block is included only if at least one `/*IF...*/` statement inside it is evaluated as true. This intelligently removes the `WHERE` keyword if no filters apply. |
|
|
171
|
+
| FOR | `/*FOR item:collection*/ ... /*END*/` | Iterates over the `collection` array from the `entity`. For each loop, the enclosed SQL is generated, and the current value is available as the `item` variable for binding. |
|
|
172
|
+
| Bind Variable | `/*variable*/` | Binds a value from the `entity`. It automatically formats values: strings are quoted (`'value'`), numbers are left as is (`123`), and arrays are turned into comma-separated lists in parentheses (`('a','b',123)`). |
|
|
173
|
+
| END | `/*END*/` | Marks the end of an `IF`, `BEGIN`, or `FOR` block. |
|
|
174
|
+
|
|
175
|
+
## 📜 License
|
|
176
|
+
|
|
177
|
+
This project is licensed under the MIT License. See the [LICENSE](https://opensource.org/licenses/MIT) file for details.
|
|
178
|
+
|
|
179
|
+
## 🎓 Advanced Usage & Examples
|
|
180
|
+
|
|
181
|
+
This README covers the basic usage of the library. For more advanced use cases and a comprehensive look at how to verify its behavior, the test suite serves as practical and up-to-date documentation.
|
|
182
|
+
|
|
183
|
+
We recommend Browse the test files to understand how to handle and verify the sequential, race-condition-free execution in various scenarios.
|
|
184
|
+
|
|
185
|
+
You can find the test case in the `/test/specs` directory of our GitHub repository.
|
|
186
|
+
|
|
187
|
+
- **[Explore our Test Suite for Advanced Examples](https://github.com/digitalwalletcorp/sql-builder/tree/main/test/specs)**
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './sql-builder';
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./sql-builder"), exports);
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,gDAA8B"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 動的SQLを生成する
|
|
3
|
+
*
|
|
4
|
+
* このクラスは、S2Daoが提供していた機能を模したもので、SQLテンプレートとバインドエンティティを渡すことで動的にSQLを生成する。
|
|
5
|
+
*
|
|
6
|
+
* 例)
|
|
7
|
+
* テンプレート
|
|
8
|
+
* ```
|
|
9
|
+
* SELECT COUNT(*) AS cnt FROM activity
|
|
10
|
+
* \/*BEGIN*\/WHERE
|
|
11
|
+
* 1 = 1
|
|
12
|
+
* \/*IF projectNames.length*\/AND project_name IN \/*projectNames*\/('project1')\/*END*\/
|
|
13
|
+
* \/*IF nodeNames.length*\/AND node_name IN \/*nodeNames*\/('node1')\/*END*\/
|
|
14
|
+
* \/*IF jobNames.length*\/AND job_name IN \/*jobNames*\/('job1')\/*END*\/
|
|
15
|
+
* \/*IF statuses.length*\/AND status IN \/*statuses*\/(1)\/*END*\/
|
|
16
|
+
* \/*END*\/
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* 呼び出し
|
|
20
|
+
* ```
|
|
21
|
+
* const bindEntity = {
|
|
22
|
+
* projectNames: ['pj1', 'pj2'],
|
|
23
|
+
* nodeNames: ['node1', 'node2'],
|
|
24
|
+
* jobNames: ['job1', 'job2'],
|
|
25
|
+
* statuses: [1, 2]
|
|
26
|
+
* };
|
|
27
|
+
* const sql = builder.generateSQL(template, bindEntity);
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* 結果
|
|
31
|
+
* ```
|
|
32
|
+
* SELECT COUNT(*) AS cnt FROM activity
|
|
33
|
+
* WHERE
|
|
34
|
+
* 1 = 1
|
|
35
|
+
* AND project_name IN ('pj1','pj2')
|
|
36
|
+
* AND node_name IN ('node1','node2')
|
|
37
|
+
* AND job_name IN ('job1','job2')
|
|
38
|
+
* AND status IN (1,2)
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare class SQLBuilder {
|
|
42
|
+
private REGEX_TAG_PATTERN;
|
|
43
|
+
constructor();
|
|
44
|
+
/**
|
|
45
|
+
* 指定したテンプレートにエンティティの値をバインドしたSQLを生成する
|
|
46
|
+
*
|
|
47
|
+
* @param {string} template
|
|
48
|
+
* @param {Record<string, any>} entity
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
generateSQL(template: string, entity: Record<string, any>): string;
|
|
52
|
+
/**
|
|
53
|
+
* テンプレートに含まれるタグ構成を解析してコンテキストを返す
|
|
54
|
+
*
|
|
55
|
+
* @param {string} template
|
|
56
|
+
* @returns {TagContext[]}
|
|
57
|
+
*/
|
|
58
|
+
private createTagContexts;
|
|
59
|
+
/**
|
|
60
|
+
* テンプレートを分析して生成したSQLを返す
|
|
61
|
+
*
|
|
62
|
+
* @param {SharedIndex} pos 現在処理している文字列の先頭インデックス
|
|
63
|
+
* @param {string} template
|
|
64
|
+
* @param {Record<string, any>} entity
|
|
65
|
+
* @param {TagContext[]} tagContexts
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
private parse;
|
|
69
|
+
/**
|
|
70
|
+
* ダミーパラメータの終了インデックスを返す
|
|
71
|
+
*
|
|
72
|
+
* @param {string} template
|
|
73
|
+
* @param {TagContext} tagContext
|
|
74
|
+
* @returns {number}
|
|
75
|
+
*/
|
|
76
|
+
private getDummyParamEndIndex;
|
|
77
|
+
/**
|
|
78
|
+
* IF条件が成立するか判定する
|
|
79
|
+
*
|
|
80
|
+
* @param {string} condition `params.length`や`param === 'a'`などの条件式
|
|
81
|
+
* @param {Record<string, any>} entity
|
|
82
|
+
* @returns {boolean}
|
|
83
|
+
*/
|
|
84
|
+
private evaluateCondition;
|
|
85
|
+
/**
|
|
86
|
+
* entityからparamで指定した値を文字列で取得する
|
|
87
|
+
*
|
|
88
|
+
* * 返却する値が配列の場合は丸括弧で括り、各項目をカンマで区切る
|
|
89
|
+
* ('a', 'b', 'c')
|
|
90
|
+
* (1, 2, 3)
|
|
91
|
+
* * 返却する値がstring型の場合はシングルクォートで括る
|
|
92
|
+
* 'abc'
|
|
93
|
+
* * 返却する値がnumber型の場合はそのまま返す
|
|
94
|
+
* 1234
|
|
95
|
+
*
|
|
96
|
+
* @param {string} property `obj.param1.param2`などのドットで繋いだプロパティ
|
|
97
|
+
* @param {Record<string, any>} entity
|
|
98
|
+
* @param {*} [options]
|
|
99
|
+
* ├ responseType 'string' | 'array' | 'object'
|
|
100
|
+
* @returns {string}
|
|
101
|
+
*/
|
|
102
|
+
private extractValue;
|
|
103
|
+
}
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SQLBuilder = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* 動的SQLを生成する
|
|
6
|
+
*
|
|
7
|
+
* このクラスは、S2Daoが提供していた機能を模したもので、SQLテンプレートとバインドエンティティを渡すことで動的にSQLを生成する。
|
|
8
|
+
*
|
|
9
|
+
* 例)
|
|
10
|
+
* テンプレート
|
|
11
|
+
* ```
|
|
12
|
+
* SELECT COUNT(*) AS cnt FROM activity
|
|
13
|
+
* \/*BEGIN*\/WHERE
|
|
14
|
+
* 1 = 1
|
|
15
|
+
* \/*IF projectNames.length*\/AND project_name IN \/*projectNames*\/('project1')\/*END*\/
|
|
16
|
+
* \/*IF nodeNames.length*\/AND node_name IN \/*nodeNames*\/('node1')\/*END*\/
|
|
17
|
+
* \/*IF jobNames.length*\/AND job_name IN \/*jobNames*\/('job1')\/*END*\/
|
|
18
|
+
* \/*IF statuses.length*\/AND status IN \/*statuses*\/(1)\/*END*\/
|
|
19
|
+
* \/*END*\/
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* 呼び出し
|
|
23
|
+
* ```
|
|
24
|
+
* const bindEntity = {
|
|
25
|
+
* projectNames: ['pj1', 'pj2'],
|
|
26
|
+
* nodeNames: ['node1', 'node2'],
|
|
27
|
+
* jobNames: ['job1', 'job2'],
|
|
28
|
+
* statuses: [1, 2]
|
|
29
|
+
* };
|
|
30
|
+
* const sql = builder.generateSQL(template, bindEntity);
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* 結果
|
|
34
|
+
* ```
|
|
35
|
+
* SELECT COUNT(*) AS cnt FROM activity
|
|
36
|
+
* WHERE
|
|
37
|
+
* 1 = 1
|
|
38
|
+
* AND project_name IN ('pj1','pj2')
|
|
39
|
+
* AND node_name IN ('node1','node2')
|
|
40
|
+
* AND job_name IN ('job1','job2')
|
|
41
|
+
* AND status IN (1,2)
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
class SQLBuilder {
|
|
45
|
+
REGEX_TAG_PATTERN = /\/\*(.*?)\*\//g;
|
|
46
|
+
constructor() {
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 指定したテンプレートにエンティティの値をバインドしたSQLを生成する
|
|
50
|
+
*
|
|
51
|
+
* @param {string} template
|
|
52
|
+
* @param {Record<string, any>} entity
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
generateSQL(template, entity) {
|
|
56
|
+
/**
|
|
57
|
+
* 「\/* *\/」で囲まれたすべての箇所を抽出
|
|
58
|
+
*/
|
|
59
|
+
const allMatchers = template.match(this.REGEX_TAG_PATTERN);
|
|
60
|
+
if (!allMatchers) {
|
|
61
|
+
return template;
|
|
62
|
+
}
|
|
63
|
+
const tagContexts = this.createTagContexts(template);
|
|
64
|
+
const pos = { index: 0 };
|
|
65
|
+
const result = this.parse(pos, template, entity, tagContexts);
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* テンプレートに含まれるタグ構成を解析してコンテキストを返す
|
|
70
|
+
*
|
|
71
|
+
* @param {string} template
|
|
72
|
+
* @returns {TagContext[]}
|
|
73
|
+
*/
|
|
74
|
+
createTagContexts(template) {
|
|
75
|
+
// マッチした箇所の開始インデックス、終了インデックス、および階層構造を保持するオブジェクトを構築する
|
|
76
|
+
/**
|
|
77
|
+
* 「\/* *\/」で囲まれたすべての箇所を抽出
|
|
78
|
+
*/
|
|
79
|
+
const rootMatcher = template.match(this.REGEX_TAG_PATTERN);
|
|
80
|
+
// まず最初にREGEX_TAG_PATTERNで解析した情報をそのままフラットにTagContextの配列に格納
|
|
81
|
+
let pos = 0;
|
|
82
|
+
const tagContexts = [];
|
|
83
|
+
if (rootMatcher) {
|
|
84
|
+
for (const matchContent of rootMatcher) {
|
|
85
|
+
const index = template.indexOf(matchContent, pos);
|
|
86
|
+
pos = index + 1;
|
|
87
|
+
const tagContext = {
|
|
88
|
+
type: null,
|
|
89
|
+
match: matchContent,
|
|
90
|
+
contents: '',
|
|
91
|
+
startIndex: index,
|
|
92
|
+
endIndex: index + matchContent.length,
|
|
93
|
+
sub: [],
|
|
94
|
+
parent: null,
|
|
95
|
+
status: 0
|
|
96
|
+
};
|
|
97
|
+
switch (true) {
|
|
98
|
+
case matchContent === '/*BEGIN*/': {
|
|
99
|
+
tagContext.type = 'BEGIN';
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
case matchContent.startsWith('/*IF'): {
|
|
103
|
+
tagContext.type = 'IF';
|
|
104
|
+
const contentMatcher = matchContent.match(/^\/\*IF\s+(.*?)\*\/$/);
|
|
105
|
+
tagContext.contents = contentMatcher && contentMatcher[1] || '';
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case matchContent.startsWith('/*FOR'): {
|
|
109
|
+
tagContext.type = 'FOR';
|
|
110
|
+
const contentMatcher = matchContent.match(/^\/\*FOR\s+(.*?)\*\/$/);
|
|
111
|
+
tagContext.contents = contentMatcher && contentMatcher[1] || '';
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
case matchContent === '/*END*/': {
|
|
115
|
+
tagContext.type = 'END';
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
default: {
|
|
119
|
+
tagContext.type = 'BIND';
|
|
120
|
+
const contentMatcher = matchContent.match(/\/\*(.*?)\*\//);
|
|
121
|
+
tagContext.contents = contentMatcher && contentMatcher[1] || '';
|
|
122
|
+
// ダミー値の終了位置をendIndexに設定
|
|
123
|
+
const dummyEndIndex = this.getDummyParamEndIndex(template, tagContext);
|
|
124
|
+
tagContext.endIndex = dummyEndIndex;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
tagContexts.push(tagContext);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// できあがったTagContextの配列から、BEGEN、IFの場合は次の対応するENDが出てくるまでをsubに入れ直して構造化し、
|
|
131
|
+
// 以下のような構造の変更する
|
|
132
|
+
/**
|
|
133
|
+
* ```
|
|
134
|
+
* BEGIN
|
|
135
|
+
* ├ IF
|
|
136
|
+
* ├ BIND
|
|
137
|
+
* ├ BIND
|
|
138
|
+
* ├ END
|
|
139
|
+
* ├ BIND
|
|
140
|
+
* END
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
const parentTagContexts = [];
|
|
144
|
+
const newTagContexts = [];
|
|
145
|
+
for (const tagContext of tagContexts) {
|
|
146
|
+
switch (tagContext.type) {
|
|
147
|
+
case 'BEGIN':
|
|
148
|
+
case 'IF':
|
|
149
|
+
case 'FOR': {
|
|
150
|
+
const parentTagContext = parentTagContexts[parentTagContexts.length - 1];
|
|
151
|
+
if (parentTagContext) {
|
|
152
|
+
// 親タグがある
|
|
153
|
+
tagContext.parent = parentTagContext;
|
|
154
|
+
parentTagContext.sub.push(tagContext);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
// 親タグがない(最上位)
|
|
158
|
+
newTagContexts.push(tagContext);
|
|
159
|
+
}
|
|
160
|
+
// 後続処理で自身が親になるので自身を追加
|
|
161
|
+
parentTagContexts.push(tagContext);
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
case 'END': {
|
|
165
|
+
const parentTagContext = parentTagContexts.pop();
|
|
166
|
+
// ENDのときは必ず対応するIF/BEGINがあるので、親のsubに追加
|
|
167
|
+
tagContext.parent = parentTagContext;
|
|
168
|
+
parentTagContext.sub.push(tagContext);
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
default: {
|
|
172
|
+
const parentTagContext = parentTagContexts[parentTagContexts.length - 1];
|
|
173
|
+
if (parentTagContext) {
|
|
174
|
+
// 親タグがある
|
|
175
|
+
tagContext.parent = parentTagContext;
|
|
176
|
+
parentTagContext.sub.push(tagContext);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
// 親タグがない(最上位)
|
|
180
|
+
newTagContexts.push(tagContext);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return newTagContexts;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* テンプレートを分析して生成したSQLを返す
|
|
189
|
+
*
|
|
190
|
+
* @param {SharedIndex} pos 現在処理している文字列の先頭インデックス
|
|
191
|
+
* @param {string} template
|
|
192
|
+
* @param {Record<string, any>} entity
|
|
193
|
+
* @param {TagContext[]} tagContexts
|
|
194
|
+
* @returns {string}
|
|
195
|
+
*/
|
|
196
|
+
parse(pos, template, entity, tagContexts) {
|
|
197
|
+
let result = '';
|
|
198
|
+
for (const tagContext of tagContexts) {
|
|
199
|
+
switch (tagContext.type) {
|
|
200
|
+
case 'BEGIN': {
|
|
201
|
+
result += template.substring(pos.index, tagContext.startIndex);
|
|
202
|
+
pos.index = tagContext.endIndex;
|
|
203
|
+
// BEGINのときは無条件にsubに対して再帰呼び出し
|
|
204
|
+
result += this.parse(pos, template, entity, tagContext.sub);
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
case 'IF': {
|
|
208
|
+
result += template.substring(pos.index, tagContext.startIndex);
|
|
209
|
+
pos.index = tagContext.endIndex;
|
|
210
|
+
if (this.evaluateCondition(tagContext.contents, entity)) {
|
|
211
|
+
// IF条件が成立する場合はsubに対して再帰呼び出し
|
|
212
|
+
tagContext.status = 10;
|
|
213
|
+
result += this.parse(pos, template, entity, tagContext.sub);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
// IF条件が成立しない場合は再帰呼び出しせず、subのENDタグのendIndexをposに設定
|
|
217
|
+
const endTagContext = tagContext.sub[tagContext.sub.length - 1];
|
|
218
|
+
pos.index = endTagContext.endIndex;
|
|
219
|
+
}
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
case 'FOR': {
|
|
223
|
+
const [bindName, collectionName] = tagContext.contents.split(':').map(a => a.trim());
|
|
224
|
+
const array = this.extractValue(collectionName, entity, {
|
|
225
|
+
responseType: 'array'
|
|
226
|
+
});
|
|
227
|
+
if (array) {
|
|
228
|
+
result += template.substring(pos.index, tagContext.startIndex);
|
|
229
|
+
for (const value of array) {
|
|
230
|
+
// 再帰呼び出しによりposが進むので、ループのたびにposを戻す必要がある
|
|
231
|
+
pos.index = tagContext.endIndex;
|
|
232
|
+
result += this.parse(pos, template, { [bindName]: value }, tagContext.sub);
|
|
233
|
+
// FORループするときは各行で改行する
|
|
234
|
+
result += '\n';
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case 'END': {
|
|
240
|
+
// 2025-04-13 現時点ではBEGINやIFがネストされた場合について期待通りに動作しない
|
|
241
|
+
switch (true) {
|
|
242
|
+
// BEGINの場合、subにIFタグが1つ以上あり、いずれもstatus=10(成功)になっていない
|
|
243
|
+
case tagContext.parent?.type === 'BEGIN'
|
|
244
|
+
&& !!tagContext.parent.sub.find(a => a.type === 'IF')
|
|
245
|
+
&& !tagContext.parent.sub.find(a => a.type === 'IF' && a.status === 10):
|
|
246
|
+
// IFの場合、IFのstatusがstatus=10(成功)になっていない
|
|
247
|
+
case tagContext.parent?.type === 'IF' && tagContext.parent.status !== 10:
|
|
248
|
+
pos.index = tagContext.endIndex;
|
|
249
|
+
return '';
|
|
250
|
+
default:
|
|
251
|
+
}
|
|
252
|
+
result += template.substring(pos.index, tagContext.startIndex);
|
|
253
|
+
pos.index = tagContext.endIndex;
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
case 'BIND': {
|
|
257
|
+
result += template.substring(pos.index, tagContext.startIndex);
|
|
258
|
+
pos.index = tagContext.endIndex;
|
|
259
|
+
const value = this.extractValue(tagContext.contents, entity);
|
|
260
|
+
result += value == null ? '' : String(value);
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
default:
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* ダミーパラメータの終了インデックスを返す
|
|
270
|
+
*
|
|
271
|
+
* @param {string} template
|
|
272
|
+
* @param {TagContext} tagContext
|
|
273
|
+
* @returns {number}
|
|
274
|
+
*/
|
|
275
|
+
getDummyParamEndIndex(template, tagContext) {
|
|
276
|
+
if (tagContext.type !== 'BIND') {
|
|
277
|
+
throw new Error(`${tagContext.type} に対してgetDummyParamEndIndexが呼び出されました`);
|
|
278
|
+
}
|
|
279
|
+
let quoted = false;
|
|
280
|
+
let bracket = false;
|
|
281
|
+
const chars = Array.from(template);
|
|
282
|
+
for (let i = tagContext.endIndex; i < template.length; i++) {
|
|
283
|
+
const c = chars[i];
|
|
284
|
+
if (bracket) {
|
|
285
|
+
// 丸括弧解析中
|
|
286
|
+
switch (true) {
|
|
287
|
+
case c === ')':
|
|
288
|
+
// 丸括弧終了
|
|
289
|
+
return i + 1;
|
|
290
|
+
case c === '\n':
|
|
291
|
+
throw new Error(`括弧が閉じられていません [${i}]`);
|
|
292
|
+
default:
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
else if (quoted) {
|
|
296
|
+
// クォート解析中
|
|
297
|
+
switch (true) {
|
|
298
|
+
case c === '\'':
|
|
299
|
+
// クォート終了
|
|
300
|
+
return i + 1;
|
|
301
|
+
case c === '\n':
|
|
302
|
+
throw new Error(`クォートが閉じられていません [${i}]`);
|
|
303
|
+
default:
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
switch (true) {
|
|
308
|
+
case c === '\'':
|
|
309
|
+
// クォート開始
|
|
310
|
+
quoted = true;
|
|
311
|
+
break;
|
|
312
|
+
case c === '(':
|
|
313
|
+
// 丸括弧開始
|
|
314
|
+
bracket = true;
|
|
315
|
+
break;
|
|
316
|
+
case c === ')':
|
|
317
|
+
throw new Error(`括弧が開始されていません [${i}]`);
|
|
318
|
+
case c === '*' && 1 < i && chars[i - 1] === '/':
|
|
319
|
+
// 次ノード開始
|
|
320
|
+
return i - 1;
|
|
321
|
+
case c === '-' && 1 < i && chars[i - 1] === '-':
|
|
322
|
+
// 行コメント
|
|
323
|
+
return i - 1;
|
|
324
|
+
case c === '\n':
|
|
325
|
+
if (1 < i && chars[i - 1] === '\r') {
|
|
326
|
+
// \r\n
|
|
327
|
+
return i - 1;
|
|
328
|
+
}
|
|
329
|
+
// \n
|
|
330
|
+
return i;
|
|
331
|
+
case c === ' ' || c === '\t':
|
|
332
|
+
// 空白文字
|
|
333
|
+
return i;
|
|
334
|
+
default:
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return template.length;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* IF条件が成立するか判定する
|
|
342
|
+
*
|
|
343
|
+
* @param {string} condition `params.length`や`param === 'a'`などの条件式
|
|
344
|
+
* @param {Record<string, any>} entity
|
|
345
|
+
* @returns {boolean}
|
|
346
|
+
*/
|
|
347
|
+
evaluateCondition(condition, entity) {
|
|
348
|
+
try {
|
|
349
|
+
const evaluate = new Function('entity', `
|
|
350
|
+
with (entity) {
|
|
351
|
+
try {
|
|
352
|
+
return ${condition};
|
|
353
|
+
} catch(error) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
}`);
|
|
357
|
+
return !!evaluate(entity);
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
console.warn('Error evaluating condition:', condition, entity, error);
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* entityからparamで指定した値を文字列で取得する
|
|
366
|
+
*
|
|
367
|
+
* * 返却する値が配列の場合は丸括弧で括り、各項目をカンマで区切る
|
|
368
|
+
* ('a', 'b', 'c')
|
|
369
|
+
* (1, 2, 3)
|
|
370
|
+
* * 返却する値がstring型の場合はシングルクォートで括る
|
|
371
|
+
* 'abc'
|
|
372
|
+
* * 返却する値がnumber型の場合はそのまま返す
|
|
373
|
+
* 1234
|
|
374
|
+
*
|
|
375
|
+
* @param {string} property `obj.param1.param2`などのドットで繋いだプロパティ
|
|
376
|
+
* @param {Record<string, any>} entity
|
|
377
|
+
* @param {*} [options]
|
|
378
|
+
* ├ responseType 'string' | 'array' | 'object'
|
|
379
|
+
* @returns {string}
|
|
380
|
+
*/
|
|
381
|
+
extractValue(property, entity, options) {
|
|
382
|
+
try {
|
|
383
|
+
const propertyPath = property.split('.');
|
|
384
|
+
let value = entity;
|
|
385
|
+
for (const prop of propertyPath) {
|
|
386
|
+
if (value && value.hasOwnProperty(prop)) {
|
|
387
|
+
value = value[prop];
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
let result = '';
|
|
394
|
+
switch (options?.responseType) {
|
|
395
|
+
case 'array':
|
|
396
|
+
case 'object':
|
|
397
|
+
return value;
|
|
398
|
+
default:
|
|
399
|
+
// string
|
|
400
|
+
if (Array.isArray(value)) {
|
|
401
|
+
result = `(${value.map(v => typeof v === 'string' ? `'${v}'` : v).join(',')})`;
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
result = typeof value === 'string' ? `'${value}'` : value;
|
|
405
|
+
}
|
|
406
|
+
return result;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
console.warn('Error extracting value', property, entity, error);
|
|
411
|
+
return undefined;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
exports.SQLBuilder = SQLBuilder;
|
|
416
|
+
//# sourceMappingURL=sql-builder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql-builder.js","sourceRoot":"","sources":["../src/sql-builder.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@digitalwalletcorp/sql-builder",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "This is a library for building SQL",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"types": "lib/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc --project tsconfig.build.json",
|
|
9
|
+
"version:patch": "npm version patch",
|
|
10
|
+
"version:minor": "npm version minor",
|
|
11
|
+
"version:major": "npm version major",
|
|
12
|
+
"pack": "rm -rf dest && mkdir -p dest && npm pack --pack-destination=dest",
|
|
13
|
+
"publish:npm": "bash ../bin/publish.sh",
|
|
14
|
+
"test:unit": "NODE_OPTIONS=--max-old-space-size=2048 npx jest --runInBand --logHeapUsage --detectOpenHandles --forceExit --errorOnDeprecated --coverage --silent --unhandled-rejections=strict"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"sql",
|
|
18
|
+
"dynamic",
|
|
19
|
+
"S2Dao"
|
|
20
|
+
],
|
|
21
|
+
"author": "satoshi kotaki",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/digitalwalletcorp/sql-builder.git"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/digitalwalletcorp/sql-builder#readme",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/digitalwalletcorp/sql-builder/issues"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"lib/**/*",
|
|
33
|
+
"src/**/*"
|
|
34
|
+
],
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/jest": "^29.5.14",
|
|
37
|
+
"jest": "^29.7.0",
|
|
38
|
+
"ts-jest": "^29.3.4",
|
|
39
|
+
"ts-node": "^10.9.2",
|
|
40
|
+
"typescript": "^5.8.3"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './sql-builder';
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
type TagType = 'BEGIN' | 'IF' | 'FOR' | 'END' | 'BIND';
|
|
2
|
+
type ExtractValueType<T extends 'string' | 'array' | 'object'>
|
|
3
|
+
= T extends 'string'
|
|
4
|
+
? string | undefined
|
|
5
|
+
: T extends 'array'
|
|
6
|
+
? any[] | undefined
|
|
7
|
+
: Record<string, any> | undefined;
|
|
8
|
+
|
|
9
|
+
interface TagContext {
|
|
10
|
+
type: TagType;
|
|
11
|
+
match: string;
|
|
12
|
+
contents: string;
|
|
13
|
+
startIndex: number;
|
|
14
|
+
endIndex: number;
|
|
15
|
+
sub: TagContext[];
|
|
16
|
+
parent: TagContext | null;
|
|
17
|
+
status: number; // 0: 初期、10: 成立 IFで条件が成立したかを判断するもの
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface SharedIndex {
|
|
21
|
+
index: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 動的SQLを生成する
|
|
26
|
+
*
|
|
27
|
+
* このクラスは、S2Daoが提供していた機能を模したもので、SQLテンプレートとバインドエンティティを渡すことで動的にSQLを生成する。
|
|
28
|
+
*
|
|
29
|
+
* 例)
|
|
30
|
+
* テンプレート
|
|
31
|
+
* ```
|
|
32
|
+
* SELECT COUNT(*) AS cnt FROM activity
|
|
33
|
+
* \/*BEGIN*\/WHERE
|
|
34
|
+
* 1 = 1
|
|
35
|
+
* \/*IF projectNames.length*\/AND project_name IN \/*projectNames*\/('project1')\/*END*\/
|
|
36
|
+
* \/*IF nodeNames.length*\/AND node_name IN \/*nodeNames*\/('node1')\/*END*\/
|
|
37
|
+
* \/*IF jobNames.length*\/AND job_name IN \/*jobNames*\/('job1')\/*END*\/
|
|
38
|
+
* \/*IF statuses.length*\/AND status IN \/*statuses*\/(1)\/*END*\/
|
|
39
|
+
* \/*END*\/
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* 呼び出し
|
|
43
|
+
* ```
|
|
44
|
+
* const bindEntity = {
|
|
45
|
+
* projectNames: ['pj1', 'pj2'],
|
|
46
|
+
* nodeNames: ['node1', 'node2'],
|
|
47
|
+
* jobNames: ['job1', 'job2'],
|
|
48
|
+
* statuses: [1, 2]
|
|
49
|
+
* };
|
|
50
|
+
* const sql = builder.generateSQL(template, bindEntity);
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* 結果
|
|
54
|
+
* ```
|
|
55
|
+
* SELECT COUNT(*) AS cnt FROM activity
|
|
56
|
+
* WHERE
|
|
57
|
+
* 1 = 1
|
|
58
|
+
* AND project_name IN ('pj1','pj2')
|
|
59
|
+
* AND node_name IN ('node1','node2')
|
|
60
|
+
* AND job_name IN ('job1','job2')
|
|
61
|
+
* AND status IN (1,2)
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export class SQLBuilder {
|
|
65
|
+
|
|
66
|
+
private REGEX_TAG_PATTERN = /\/\*(.*?)\*\//g;
|
|
67
|
+
|
|
68
|
+
constructor() {
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 指定したテンプレートにエンティティの値をバインドしたSQLを生成する
|
|
73
|
+
*
|
|
74
|
+
* @param {string} template
|
|
75
|
+
* @param {Record<string, any>} entity
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
public generateSQL(template: string, entity: Record<string, any>): string {
|
|
79
|
+
/**
|
|
80
|
+
* 「\/* *\/」で囲まれたすべての箇所を抽出
|
|
81
|
+
*/
|
|
82
|
+
const allMatchers = template.match(this.REGEX_TAG_PATTERN);
|
|
83
|
+
if (!allMatchers) {
|
|
84
|
+
return template;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const tagContexts = this.createTagContexts(template);
|
|
88
|
+
const pos: SharedIndex = { index: 0 };
|
|
89
|
+
const result = this.parse(pos, template, entity, tagContexts);
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* テンプレートに含まれるタグ構成を解析してコンテキストを返す
|
|
95
|
+
*
|
|
96
|
+
* @param {string} template
|
|
97
|
+
* @returns {TagContext[]}
|
|
98
|
+
*/
|
|
99
|
+
private createTagContexts(template: string): TagContext[] {
|
|
100
|
+
// マッチした箇所の開始インデックス、終了インデックス、および階層構造を保持するオブジェクトを構築する
|
|
101
|
+
/**
|
|
102
|
+
* 「\/* *\/」で囲まれたすべての箇所を抽出
|
|
103
|
+
*/
|
|
104
|
+
const rootMatcher = template.match(this.REGEX_TAG_PATTERN);
|
|
105
|
+
|
|
106
|
+
// まず最初にREGEX_TAG_PATTERNで解析した情報をそのままフラットにTagContextの配列に格納
|
|
107
|
+
let pos = 0;
|
|
108
|
+
const tagContexts: TagContext[] = [];
|
|
109
|
+
if (rootMatcher) {
|
|
110
|
+
for (const matchContent of rootMatcher) {
|
|
111
|
+
const index = template.indexOf(matchContent, pos);
|
|
112
|
+
pos = index + 1;
|
|
113
|
+
const tagContext: TagContext = {
|
|
114
|
+
type: null as unknown as TagType,
|
|
115
|
+
match: matchContent,
|
|
116
|
+
contents: '',
|
|
117
|
+
startIndex: index,
|
|
118
|
+
endIndex: index + matchContent.length,
|
|
119
|
+
sub: [],
|
|
120
|
+
parent: null,
|
|
121
|
+
status: 0
|
|
122
|
+
};
|
|
123
|
+
switch (true) {
|
|
124
|
+
case matchContent === '/*BEGIN*/': {
|
|
125
|
+
tagContext.type = 'BEGIN';
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case matchContent.startsWith('/*IF'): {
|
|
129
|
+
tagContext.type = 'IF';
|
|
130
|
+
const contentMatcher = matchContent.match(/^\/\*IF\s+(.*?)\*\/$/);
|
|
131
|
+
tagContext.contents = contentMatcher && contentMatcher[1] || '';
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
case matchContent.startsWith('/*FOR'): {
|
|
135
|
+
tagContext.type = 'FOR';
|
|
136
|
+
const contentMatcher = matchContent.match(/^\/\*FOR\s+(.*?)\*\/$/);
|
|
137
|
+
tagContext.contents = contentMatcher && contentMatcher[1] || '';
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case matchContent === '/*END*/': {
|
|
141
|
+
tagContext.type = 'END';
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
default: {
|
|
145
|
+
tagContext.type = 'BIND';
|
|
146
|
+
const contentMatcher = matchContent.match(/\/\*(.*?)\*\//);
|
|
147
|
+
tagContext.contents = contentMatcher && contentMatcher[1] || '';
|
|
148
|
+
// ダミー値の終了位置をendIndexに設定
|
|
149
|
+
const dummyEndIndex = this.getDummyParamEndIndex(template, tagContext);
|
|
150
|
+
tagContext.endIndex = dummyEndIndex;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
tagContexts.push(tagContext);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// できあがったTagContextの配列から、BEGEN、IFの場合は次の対応するENDが出てくるまでをsubに入れ直して構造化し、
|
|
158
|
+
// 以下のような構造の変更する
|
|
159
|
+
/**
|
|
160
|
+
* ```
|
|
161
|
+
* BEGIN
|
|
162
|
+
* ├ IF
|
|
163
|
+
* ├ BIND
|
|
164
|
+
* ├ BIND
|
|
165
|
+
* ├ END
|
|
166
|
+
* ├ BIND
|
|
167
|
+
* END
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
const parentTagContexts: TagContext[] = [];
|
|
171
|
+
const newTagContexts: TagContext[] = [];
|
|
172
|
+
for (const tagContext of tagContexts) {
|
|
173
|
+
switch (tagContext.type) {
|
|
174
|
+
case 'BEGIN':
|
|
175
|
+
case 'IF':
|
|
176
|
+
case 'FOR': {
|
|
177
|
+
const parentTagContext = parentTagContexts[parentTagContexts.length - 1];
|
|
178
|
+
if (parentTagContext) {
|
|
179
|
+
// 親タグがある
|
|
180
|
+
tagContext.parent = parentTagContext;
|
|
181
|
+
parentTagContext.sub.push(tagContext);
|
|
182
|
+
} else {
|
|
183
|
+
// 親タグがない(最上位)
|
|
184
|
+
newTagContexts.push(tagContext);
|
|
185
|
+
}
|
|
186
|
+
// 後続処理で自身が親になるので自身を追加
|
|
187
|
+
parentTagContexts.push(tagContext);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
case 'END': {
|
|
191
|
+
const parentTagContext = parentTagContexts.pop()!;
|
|
192
|
+
// ENDのときは必ず対応するIF/BEGINがあるので、親のsubに追加
|
|
193
|
+
tagContext.parent = parentTagContext;
|
|
194
|
+
parentTagContext.sub.push(tagContext);
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
default: {
|
|
198
|
+
const parentTagContext = parentTagContexts[parentTagContexts.length - 1];
|
|
199
|
+
if (parentTagContext) {
|
|
200
|
+
// 親タグがある
|
|
201
|
+
tagContext.parent = parentTagContext;
|
|
202
|
+
parentTagContext.sub.push(tagContext);
|
|
203
|
+
} else {
|
|
204
|
+
// 親タグがない(最上位)
|
|
205
|
+
newTagContexts.push(tagContext);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return newTagContexts;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* テンプレートを分析して生成したSQLを返す
|
|
216
|
+
*
|
|
217
|
+
* @param {SharedIndex} pos 現在処理している文字列の先頭インデックス
|
|
218
|
+
* @param {string} template
|
|
219
|
+
* @param {Record<string, any>} entity
|
|
220
|
+
* @param {TagContext[]} tagContexts
|
|
221
|
+
* @returns {string}
|
|
222
|
+
*/
|
|
223
|
+
private parse(pos: SharedIndex, template: string, entity: Record<string, any>, tagContexts: TagContext[]): string {
|
|
224
|
+
let result = '';
|
|
225
|
+
for (const tagContext of tagContexts) {
|
|
226
|
+
switch (tagContext.type) {
|
|
227
|
+
case 'BEGIN': {
|
|
228
|
+
result += template.substring(pos.index, tagContext.startIndex);
|
|
229
|
+
pos.index = tagContext.endIndex;
|
|
230
|
+
// BEGINのときは無条件にsubに対して再帰呼び出し
|
|
231
|
+
result += this.parse(pos, template, entity, tagContext.sub);
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
case 'IF': {
|
|
235
|
+
result += template.substring(pos.index, tagContext.startIndex);
|
|
236
|
+
pos.index = tagContext.endIndex;
|
|
237
|
+
if (this.evaluateCondition(tagContext.contents, entity)) {
|
|
238
|
+
// IF条件が成立する場合はsubに対して再帰呼び出し
|
|
239
|
+
tagContext.status = 10;
|
|
240
|
+
result += this.parse(pos, template, entity, tagContext.sub);
|
|
241
|
+
} else {
|
|
242
|
+
// IF条件が成立しない場合は再帰呼び出しせず、subのENDタグのendIndexをposに設定
|
|
243
|
+
const endTagContext = tagContext.sub[tagContext.sub.length - 1];
|
|
244
|
+
pos.index = endTagContext.endIndex;
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
case 'FOR': {
|
|
249
|
+
const [bindName, collectionName] = tagContext.contents.split(':').map(a => a.trim());
|
|
250
|
+
const array = this.extractValue(collectionName, entity, {
|
|
251
|
+
responseType: 'array'
|
|
252
|
+
});
|
|
253
|
+
if (array) {
|
|
254
|
+
result += template.substring(pos.index, tagContext.startIndex);
|
|
255
|
+
for (const value of array) {
|
|
256
|
+
// 再帰呼び出しによりposが進むので、ループのたびにposを戻す必要がある
|
|
257
|
+
pos.index = tagContext.endIndex;
|
|
258
|
+
result += this.parse(pos, template, { [bindName]: value }, tagContext.sub);
|
|
259
|
+
// FORループするときは各行で改行する
|
|
260
|
+
result += '\n';
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
case 'END': {
|
|
266
|
+
// 2025-04-13 現時点ではBEGINやIFがネストされた場合について期待通りに動作しない
|
|
267
|
+
switch (true) {
|
|
268
|
+
// BEGINの場合、subにIFタグが1つ以上あり、いずれもstatus=10(成功)になっていない
|
|
269
|
+
case tagContext.parent?.type === 'BEGIN'
|
|
270
|
+
&& !!tagContext.parent.sub.find(a => a.type === 'IF')
|
|
271
|
+
&& !tagContext.parent.sub.find(a => a.type === 'IF' && a.status === 10):
|
|
272
|
+
// IFの場合、IFのstatusがstatus=10(成功)になっていない
|
|
273
|
+
case tagContext.parent?.type === 'IF' && tagContext.parent.status !== 10:
|
|
274
|
+
pos.index = tagContext.endIndex;
|
|
275
|
+
return '';
|
|
276
|
+
default:
|
|
277
|
+
}
|
|
278
|
+
result += template.substring(pos.index, tagContext.startIndex);
|
|
279
|
+
pos.index = tagContext.endIndex;
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
case 'BIND': {
|
|
283
|
+
result += template.substring(pos.index, tagContext.startIndex);
|
|
284
|
+
pos.index = tagContext.endIndex;
|
|
285
|
+
const value = this.extractValue(tagContext.contents, entity);
|
|
286
|
+
result += value == null ? '' : String(value);
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
default:
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* ダミーパラメータの終了インデックスを返す
|
|
298
|
+
*
|
|
299
|
+
* @param {string} template
|
|
300
|
+
* @param {TagContext} tagContext
|
|
301
|
+
* @returns {number}
|
|
302
|
+
*/
|
|
303
|
+
private getDummyParamEndIndex(template: string, tagContext: TagContext): number {
|
|
304
|
+
if (tagContext.type !== 'BIND') {
|
|
305
|
+
throw new Error(`${tagContext.type} に対してgetDummyParamEndIndexが呼び出されました`);
|
|
306
|
+
}
|
|
307
|
+
let quoted = false;
|
|
308
|
+
let bracket = false;
|
|
309
|
+
const chars: string[] = Array.from(template);
|
|
310
|
+
for (let i = tagContext.endIndex; i < template.length; i++) {
|
|
311
|
+
const c = chars[i];
|
|
312
|
+
if (bracket) {
|
|
313
|
+
// 丸括弧解析中
|
|
314
|
+
switch (true) {
|
|
315
|
+
case c === ')':
|
|
316
|
+
// 丸括弧終了
|
|
317
|
+
return i + 1;
|
|
318
|
+
case c === '\n':
|
|
319
|
+
throw new Error(`括弧が閉じられていません [${i}]`);
|
|
320
|
+
default:
|
|
321
|
+
}
|
|
322
|
+
} else if (quoted) {
|
|
323
|
+
// クォート解析中
|
|
324
|
+
switch (true) {
|
|
325
|
+
case c === '\'':
|
|
326
|
+
// クォート終了
|
|
327
|
+
return i + 1;
|
|
328
|
+
case c === '\n':
|
|
329
|
+
throw new Error(`クォートが閉じられていません [${i}]`);
|
|
330
|
+
default:
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
switch (true) {
|
|
334
|
+
case c === '\'':
|
|
335
|
+
// クォート開始
|
|
336
|
+
quoted = true;
|
|
337
|
+
break;
|
|
338
|
+
case c === '(':
|
|
339
|
+
// 丸括弧開始
|
|
340
|
+
bracket = true;
|
|
341
|
+
break;
|
|
342
|
+
case c === ')':
|
|
343
|
+
throw new Error(`括弧が開始されていません [${i}]`);
|
|
344
|
+
case c === '*' && 1 < i && chars[i - 1] === '/':
|
|
345
|
+
// 次ノード開始
|
|
346
|
+
return i - 1;
|
|
347
|
+
case c === '-' && 1 < i && chars[i - 1] === '-':
|
|
348
|
+
// 行コメント
|
|
349
|
+
return i - 1;
|
|
350
|
+
case c === '\n':
|
|
351
|
+
if (1 < i && chars[i - 1] === '\r') {
|
|
352
|
+
// \r\n
|
|
353
|
+
return i - 1;
|
|
354
|
+
}
|
|
355
|
+
// \n
|
|
356
|
+
return i;
|
|
357
|
+
case c === ' ' || c === '\t':
|
|
358
|
+
// 空白文字
|
|
359
|
+
return i;
|
|
360
|
+
default:
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return template.length;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* IF条件が成立するか判定する
|
|
369
|
+
*
|
|
370
|
+
* @param {string} condition `params.length`や`param === 'a'`などの条件式
|
|
371
|
+
* @param {Record<string, any>} entity
|
|
372
|
+
* @returns {boolean}
|
|
373
|
+
*/
|
|
374
|
+
private evaluateCondition(condition: string, entity: Record<string, any>): boolean {
|
|
375
|
+
try {
|
|
376
|
+
const evaluate = new Function('entity', `
|
|
377
|
+
with (entity) {
|
|
378
|
+
try {
|
|
379
|
+
return ${condition};
|
|
380
|
+
} catch(error) {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
}`
|
|
384
|
+
);
|
|
385
|
+
return !!evaluate(entity);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
console.warn('Error evaluating condition:', condition, entity, error);
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* entityからparamで指定した値を文字列で取得する
|
|
394
|
+
*
|
|
395
|
+
* * 返却する値が配列の場合は丸括弧で括り、各項目をカンマで区切る
|
|
396
|
+
* ('a', 'b', 'c')
|
|
397
|
+
* (1, 2, 3)
|
|
398
|
+
* * 返却する値がstring型の場合はシングルクォートで括る
|
|
399
|
+
* 'abc'
|
|
400
|
+
* * 返却する値がnumber型の場合はそのまま返す
|
|
401
|
+
* 1234
|
|
402
|
+
*
|
|
403
|
+
* @param {string} property `obj.param1.param2`などのドットで繋いだプロパティ
|
|
404
|
+
* @param {Record<string, any>} entity
|
|
405
|
+
* @param {*} [options]
|
|
406
|
+
* ├ responseType 'string' | 'array' | 'object'
|
|
407
|
+
* @returns {string}
|
|
408
|
+
*/
|
|
409
|
+
private extractValue<T extends 'string' | 'array' | 'object' = 'string'>(property: string, entity: Record<string, any>, options?: {
|
|
410
|
+
responseType?: T
|
|
411
|
+
}): ExtractValueType<T> {
|
|
412
|
+
try {
|
|
413
|
+
const propertyPath = property.split('.');
|
|
414
|
+
let value: any = entity;
|
|
415
|
+
for (const prop of propertyPath) {
|
|
416
|
+
if (value && value.hasOwnProperty(prop)) {
|
|
417
|
+
value = value[prop];
|
|
418
|
+
} else {
|
|
419
|
+
return undefined as ExtractValueType<T>;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
let result = '';
|
|
423
|
+
switch (options?.responseType) {
|
|
424
|
+
case 'array':
|
|
425
|
+
case 'object':
|
|
426
|
+
return value as ExtractValueType<T>;
|
|
427
|
+
default:
|
|
428
|
+
// string
|
|
429
|
+
if (Array.isArray(value)) {
|
|
430
|
+
result = `(${value.map(v => typeof v === 'string' ? `'${v}'` : v).join(',')})`;
|
|
431
|
+
} else {
|
|
432
|
+
result = typeof value === 'string' ? `'${value}'` : value;
|
|
433
|
+
}
|
|
434
|
+
return result as ExtractValueType<T>;
|
|
435
|
+
}
|
|
436
|
+
} catch (error) {
|
|
437
|
+
console.warn('Error extracting value', property, entity, error);
|
|
438
|
+
return undefined as ExtractValueType<T>;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|