uranio 1.1.7 → 1.1.9
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/.uranio/package.json +1 -1
- package/.uranio/src/atom/mysql.ts +12 -12
- package/.uranio/src/atom/postgresql.ts +12 -12
- package/.uranio/src/client/mongodb.ts +4 -4
- package/.uranio/src/client/mysql.ts +9 -2
- package/.uranio/src/client/postgresql.ts +11 -2
- package/.uranio/src/sql/builder.ts +315 -0
- package/.uranio/src/sql/index.ts +3 -4
- package/.uranio/src/sql/template.ts +100 -0
- package/package.json +1 -1
- package/.uranio/src/sql/full_query.ts +0 -378
- package/.uranio/src/sql/param_query.ts +0 -268
- package/.uranio/tests/unit/sql/full_query.test.ts +0 -545
- package/.uranio/tests/unit/sql/param_query_comprehensive.test.ts +0 -437
- package/.uranio/tests/unit/sql/value_formatting.test.ts +0 -523
package/.uranio/package.json
CHANGED
|
@@ -20,12 +20,12 @@ export class MySQLAtomClient<S extends atom_types.mysql_atom> {
|
|
|
20
20
|
) {}
|
|
21
21
|
|
|
22
22
|
public async getAtom(where: where_types.Where<S>): Promise<S | null> {
|
|
23
|
-
const
|
|
23
|
+
const query = sql.build.select({
|
|
24
24
|
table: this.name,
|
|
25
25
|
where,
|
|
26
26
|
limit: '1',
|
|
27
27
|
});
|
|
28
|
-
const [rows] = await this.client.exe(query
|
|
28
|
+
const [rows] = await this.client.exe(query);
|
|
29
29
|
if (Array.isArray(rows) && rows[0]) {
|
|
30
30
|
return rows[0] as S;
|
|
31
31
|
}
|
|
@@ -41,13 +41,13 @@ export class MySQLAtomClient<S extends atom_types.mysql_atom> {
|
|
|
41
41
|
order?: sql_types.OrderBy;
|
|
42
42
|
limit?: string;
|
|
43
43
|
}): Promise<S[]> {
|
|
44
|
-
const
|
|
44
|
+
const query = sql.build.select({
|
|
45
45
|
table: this.name,
|
|
46
46
|
where,
|
|
47
47
|
order,
|
|
48
48
|
limit,
|
|
49
49
|
});
|
|
50
|
-
const [rows] = await this.client.exe(query
|
|
50
|
+
const [rows] = await this.client.exe(query);
|
|
51
51
|
if (Array.isArray(rows)) {
|
|
52
52
|
return rows as S[];
|
|
53
53
|
}
|
|
@@ -59,12 +59,12 @@ export class MySQLAtomClient<S extends atom_types.mysql_atom> {
|
|
|
59
59
|
for(const [key, _] of Object.entries(shape)){
|
|
60
60
|
columns.push(key as keyof S);
|
|
61
61
|
}
|
|
62
|
-
const
|
|
62
|
+
const query = sql.build.insert({
|
|
63
63
|
table: this.name,
|
|
64
64
|
columns,
|
|
65
65
|
records: [shape]
|
|
66
66
|
});
|
|
67
|
-
const response = await this.client.exe(query
|
|
67
|
+
const response = await this.client.exe(query);
|
|
68
68
|
return response;
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -76,12 +76,12 @@ export class MySQLAtomClient<S extends atom_types.mysql_atom> {
|
|
|
76
76
|
for(const [key, _] of Object.entries(shapes[0])){
|
|
77
77
|
columns.push(key as keyof S);
|
|
78
78
|
}
|
|
79
|
-
const
|
|
79
|
+
const query = sql.build.insert({
|
|
80
80
|
table: this.name,
|
|
81
81
|
columns,
|
|
82
82
|
records: shapes
|
|
83
83
|
});
|
|
84
|
-
const response = await this.client.exe(query
|
|
84
|
+
const response = await this.client.exe(query);
|
|
85
85
|
return response;
|
|
86
86
|
}
|
|
87
87
|
|
|
@@ -89,21 +89,21 @@ export class MySQLAtomClient<S extends atom_types.mysql_atom> {
|
|
|
89
89
|
atom: Partial<S>,
|
|
90
90
|
where?: where_types.Where<S>,
|
|
91
91
|
): Promise<any> {
|
|
92
|
-
const
|
|
92
|
+
const query = sql.build.update({
|
|
93
93
|
table: this.name,
|
|
94
94
|
where,
|
|
95
95
|
update: atom
|
|
96
96
|
});
|
|
97
|
-
const response = await this.client.exe(query
|
|
97
|
+
const response = await this.client.exe(query);
|
|
98
98
|
return response;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
public async deleteAtoms(where: where_types.Where<S>): Promise<any> {
|
|
102
|
-
const
|
|
102
|
+
const query = sql.build.del({
|
|
103
103
|
table: this.name,
|
|
104
104
|
where,
|
|
105
105
|
});
|
|
106
|
-
const response = await this.client.exe(query
|
|
106
|
+
const response = await this.client.exe(query);
|
|
107
107
|
return response;
|
|
108
108
|
}
|
|
109
109
|
}
|
|
@@ -20,12 +20,12 @@ export class PostgreSQLAtomClient<S extends atom_types.postgresql_atom> {
|
|
|
20
20
|
) {}
|
|
21
21
|
|
|
22
22
|
public async getAtom(where: where_types.Where<S>): Promise<S | null> {
|
|
23
|
-
const
|
|
23
|
+
const query = sql.build.select({
|
|
24
24
|
table: this.name,
|
|
25
25
|
where,
|
|
26
26
|
limit: '1',
|
|
27
27
|
});
|
|
28
|
-
const [rows] = await this.client.exe(query
|
|
28
|
+
const [rows] = await this.client.exe(query);
|
|
29
29
|
if (Array.isArray(rows) && rows[0]) {
|
|
30
30
|
return rows[0] as S;
|
|
31
31
|
}
|
|
@@ -41,13 +41,13 @@ export class PostgreSQLAtomClient<S extends atom_types.postgresql_atom> {
|
|
|
41
41
|
order?: sql_types.OrderBy;
|
|
42
42
|
limit?: string;
|
|
43
43
|
}): Promise<S[]> {
|
|
44
|
-
const
|
|
44
|
+
const query = sql.build.select({
|
|
45
45
|
table: this.name,
|
|
46
46
|
where,
|
|
47
47
|
order,
|
|
48
48
|
limit,
|
|
49
49
|
});
|
|
50
|
-
const [rows] = await this.client.exe(query
|
|
50
|
+
const [rows] = await this.client.exe(query);
|
|
51
51
|
if (Array.isArray(rows)) {
|
|
52
52
|
return rows as S[];
|
|
53
53
|
}
|
|
@@ -59,12 +59,12 @@ export class PostgreSQLAtomClient<S extends atom_types.postgresql_atom> {
|
|
|
59
59
|
for(const [key, _] of Object.entries(shape)){
|
|
60
60
|
columns.push(key as keyof S);
|
|
61
61
|
}
|
|
62
|
-
const
|
|
62
|
+
const query = sql.build.insert({
|
|
63
63
|
table: this.name,
|
|
64
64
|
columns,
|
|
65
65
|
records: [shape]
|
|
66
66
|
});
|
|
67
|
-
const response = await this.client.exe(query
|
|
67
|
+
const response = await this.client.exe(query);
|
|
68
68
|
return response;
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -76,12 +76,12 @@ export class PostgreSQLAtomClient<S extends atom_types.postgresql_atom> {
|
|
|
76
76
|
for(const [key, _] of Object.entries(shapes[0])){
|
|
77
77
|
columns.push(key as keyof S);
|
|
78
78
|
}
|
|
79
|
-
const
|
|
79
|
+
const query = sql.build.insert({
|
|
80
80
|
table: this.name,
|
|
81
81
|
columns,
|
|
82
82
|
records: shapes
|
|
83
83
|
});
|
|
84
|
-
const response = await this.client.exe(query
|
|
84
|
+
const response = await this.client.exe(query);
|
|
85
85
|
return response;
|
|
86
86
|
}
|
|
87
87
|
|
|
@@ -89,21 +89,21 @@ export class PostgreSQLAtomClient<S extends atom_types.postgresql_atom> {
|
|
|
89
89
|
atom: Partial<S>,
|
|
90
90
|
where?: where_types.Where<S>,
|
|
91
91
|
): Promise<any> {
|
|
92
|
-
const
|
|
92
|
+
const query = sql.build.update({
|
|
93
93
|
table: this.name,
|
|
94
94
|
where,
|
|
95
95
|
update: atom
|
|
96
96
|
});
|
|
97
|
-
const response = await this.client.exe(query
|
|
97
|
+
const response = await this.client.exe(query);
|
|
98
98
|
return response;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
public async deleteAtoms(where: where_types.Where<S>): Promise<any> {
|
|
102
|
-
const
|
|
102
|
+
const query = sql.build.del({
|
|
103
103
|
table: this.name,
|
|
104
104
|
where,
|
|
105
105
|
});
|
|
106
|
-
const response = await this.client.exe(query
|
|
106
|
+
const response = await this.client.exe(query);
|
|
107
107
|
return response;
|
|
108
108
|
}
|
|
109
109
|
}
|
|
@@ -38,16 +38,16 @@ export class MongoDBClient {
|
|
|
38
38
|
try {
|
|
39
39
|
// Check if the client is already connected by pinging the database
|
|
40
40
|
await this.client.db().command({ping: 1});
|
|
41
|
-
log.
|
|
41
|
+
log.debug('MongoDB is already connected.');
|
|
42
42
|
} catch (error) {
|
|
43
43
|
log.trace('Connecting to MongoDB...');
|
|
44
44
|
await this.client.connect();
|
|
45
|
-
log.
|
|
45
|
+
log.debug('MongoDB connected.');
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
public async disconnect() {
|
|
49
|
-
log.trace('Disconnecting...');
|
|
49
|
+
log.trace('Disconnecting from MongoDB...');
|
|
50
50
|
await this.client.close();
|
|
51
|
-
log.
|
|
51
|
+
log.debug('MongoDB disconnected.');
|
|
52
52
|
}
|
|
53
53
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import mysql from 'mysql2/promise';
|
|
10
10
|
import {log} from '../log/index';
|
|
11
|
+
import {SQLStatement} from '../sql/template';
|
|
11
12
|
|
|
12
13
|
export type MySQLClientParams = {
|
|
13
14
|
uri: string;
|
|
@@ -31,12 +32,18 @@ export class MySQLClient {
|
|
|
31
32
|
});
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
|
-
public async exe(sql: string, values?: any) {
|
|
35
|
+
public async exe(sql: string | SQLStatement, values?: any): Promise<any[]> {
|
|
36
|
+
// Handle SQLStatement objects
|
|
37
|
+
if (sql instanceof SQLStatement) {
|
|
38
|
+
const {sql: query, values: params} = sql.mysql();
|
|
39
|
+
return this.exe(query, params);
|
|
40
|
+
}
|
|
41
|
+
|
|
35
42
|
const with_values =
|
|
36
43
|
typeof values !== 'undefined'
|
|
37
44
|
? ` with values [${Object.entries(values)}]`
|
|
38
45
|
: '';
|
|
39
|
-
log.
|
|
46
|
+
log.debug(`Excuting query '${sql}'${with_values}`);
|
|
40
47
|
if (this.pool) {
|
|
41
48
|
return await this._execute_from_pool_connection(sql, values);
|
|
42
49
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import {Pool, PoolClient} from 'pg';
|
|
10
10
|
import {log} from '../log/index';
|
|
11
|
+
import {SQLStatement} from '../sql/template';
|
|
11
12
|
|
|
12
13
|
export type PostgreSQLClientParams = {
|
|
13
14
|
uri: string;
|
|
@@ -26,14 +27,22 @@ export class PostgreSQLClient {
|
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
|
-
public async exe(sql: string, values?: any) {
|
|
30
|
+
public async exe(sql: string | SQLStatement, values?: any): Promise<any[]> {
|
|
31
|
+
// Handle SQLStatement objects
|
|
32
|
+
if (sql instanceof SQLStatement) {
|
|
33
|
+
const {sql: query, values: params} = sql.postgres();
|
|
34
|
+
// Convert backticks to double quotes for PostgreSQL
|
|
35
|
+
const pgQuery = query.replace(/`/g, '"');
|
|
36
|
+
return this.exe(pgQuery, params);
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
// Convert named parameters object to positional parameters array
|
|
31
40
|
const {query, paramValues} = this._convert_named_to_positional(sql, values);
|
|
32
41
|
const with_values =
|
|
33
42
|
typeof paramValues !== 'undefined' && paramValues.length > 0
|
|
34
43
|
? ` with values [${paramValues}]`
|
|
35
44
|
: '';
|
|
36
|
-
log.
|
|
45
|
+
log.debug(`Excuting query '${query}'${with_values}`);
|
|
37
46
|
if (this.pool) {
|
|
38
47
|
return await this._execute_from_pool_connection(query, paramValues);
|
|
39
48
|
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simplified SQL query builder
|
|
3
|
+
* Much simpler than full_query.ts + param_query.ts
|
|
4
|
+
*
|
|
5
|
+
* Key differences:
|
|
6
|
+
* - Uses template literals internally (no string replacement bugs)
|
|
7
|
+
* - Direct parameterization (no two-stage conversion)
|
|
8
|
+
* - Each value gets its own parameter (no duplicate value issues)
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {SQL, SQLStatement} from './template';
|
|
14
|
+
import * as atom_types from '../types/atom';
|
|
15
|
+
import * as where_types from '../types/where';
|
|
16
|
+
import * as sql_types from '../types/sql';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build a SELECT query
|
|
20
|
+
*/
|
|
21
|
+
export function select<S extends atom_types.atom>({
|
|
22
|
+
projection = ['*'],
|
|
23
|
+
table,
|
|
24
|
+
where,
|
|
25
|
+
order,
|
|
26
|
+
limit,
|
|
27
|
+
}: {
|
|
28
|
+
projection?: string[];
|
|
29
|
+
table: string;
|
|
30
|
+
where?: where_types.Where<S>;
|
|
31
|
+
order?: sql_types.OrderBy;
|
|
32
|
+
limit?: string;
|
|
33
|
+
}): SQLStatement {
|
|
34
|
+
// Start with SELECT columns FROM table
|
|
35
|
+
const cols = projection.join(', ');
|
|
36
|
+
let query = new SQLStatement([`SELECT ${cols} FROM \`${table}\``], []);
|
|
37
|
+
|
|
38
|
+
// Add WHERE clause
|
|
39
|
+
if (where && Object.keys(where).length > 0) {
|
|
40
|
+
const whereStmt = _where(where);
|
|
41
|
+
query.append(' WHERE ').append(whereStmt);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Add ORDER BY
|
|
45
|
+
if (order && Object.keys(order).length > 0) {
|
|
46
|
+
query.append(_orderBy(order));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Add LIMIT
|
|
50
|
+
if (limit) {
|
|
51
|
+
query.append(_limit(limit));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return query;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build an INSERT query
|
|
59
|
+
*/
|
|
60
|
+
export function insert<S extends atom_types.atom>({
|
|
61
|
+
table,
|
|
62
|
+
columns,
|
|
63
|
+
records,
|
|
64
|
+
}: {
|
|
65
|
+
table: string;
|
|
66
|
+
columns: (keyof S)[];
|
|
67
|
+
records: Partial<S>[];
|
|
68
|
+
}): SQLStatement {
|
|
69
|
+
const colNames = columns.map(c => `\`${String(c)}\``).join(', ');
|
|
70
|
+
let query = new SQLStatement([`INSERT INTO \`${table}\` (${colNames}) VALUES `], []);
|
|
71
|
+
|
|
72
|
+
// Add each record as (?, ?, ?) with actual values
|
|
73
|
+
for (let i = 0; i < records.length; i++) {
|
|
74
|
+
if (i > 0) query.append(', ');
|
|
75
|
+
|
|
76
|
+
query.append('(');
|
|
77
|
+
for (let j = 0; j < columns.length; j++) {
|
|
78
|
+
if (j > 0) query.append(', ');
|
|
79
|
+
const col = columns[j];
|
|
80
|
+
const rec = records[i];
|
|
81
|
+
if (rec && col) {
|
|
82
|
+
const value = rec[col];
|
|
83
|
+
query.append(SQL`${value}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
query.append(')');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return query;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Build an UPDATE query
|
|
94
|
+
*/
|
|
95
|
+
export function update<S extends atom_types.atom>({
|
|
96
|
+
table,
|
|
97
|
+
update,
|
|
98
|
+
where,
|
|
99
|
+
}: {
|
|
100
|
+
table: string;
|
|
101
|
+
update: Partial<S>;
|
|
102
|
+
where?: where_types.Where<S>;
|
|
103
|
+
}): SQLStatement {
|
|
104
|
+
let query = new SQLStatement([`UPDATE \`${table}\` SET `], []);
|
|
105
|
+
|
|
106
|
+
// Add SET clause
|
|
107
|
+
const keys = Object.keys(update);
|
|
108
|
+
for (let i = 0; i < keys.length; i++) {
|
|
109
|
+
if (i > 0) query.append(', ');
|
|
110
|
+
const key = keys[i];
|
|
111
|
+
if (key) {
|
|
112
|
+
const value = (update as any)[key];
|
|
113
|
+
query.append(`${key} = `).append(SQL`${value}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Add WHERE clause
|
|
118
|
+
if (where) {
|
|
119
|
+
query.append(' WHERE ').append(_where(where));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return query;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Build a DELETE query
|
|
127
|
+
*/
|
|
128
|
+
export function del<S extends atom_types.atom>({
|
|
129
|
+
table,
|
|
130
|
+
where,
|
|
131
|
+
}: {
|
|
132
|
+
table: string;
|
|
133
|
+
where?: where_types.Where<S>;
|
|
134
|
+
}): SQLStatement {
|
|
135
|
+
let query = new SQLStatement([`DELETE FROM \`${table}\``], []);
|
|
136
|
+
|
|
137
|
+
if (where) {
|
|
138
|
+
query.append(' WHERE ').append(_where(where));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return query;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Build WHERE clause
|
|
146
|
+
*/
|
|
147
|
+
function _where<S extends atom_types.atom>(where: where_types.Where<S>): SQLStatement {
|
|
148
|
+
const parts: SQLStatement[] = [];
|
|
149
|
+
|
|
150
|
+
for (const [key, value] of Object.entries(where)) {
|
|
151
|
+
// Root operators: $and, $or, $nor
|
|
152
|
+
if (sql_types.root_operators.includes(key as sql_types.RootOperator)) {
|
|
153
|
+
if (!Array.isArray(value)) {
|
|
154
|
+
throw new Error(`Root operator '${key}' must have an Array as value`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const subclauses = value.map(el => _where(el));
|
|
158
|
+
const op = key === '$and' ? ' AND ' : key === '$or' ? ' OR ' : ' NOR ';
|
|
159
|
+
|
|
160
|
+
let combined = new SQLStatement(['('], []);
|
|
161
|
+
const firstClause = subclauses[0];
|
|
162
|
+
if (firstClause) {
|
|
163
|
+
combined.append(firstClause);
|
|
164
|
+
}
|
|
165
|
+
for (let i = 1; i < subclauses.length; i++) {
|
|
166
|
+
const clause = subclauses[i];
|
|
167
|
+
if (clause) {
|
|
168
|
+
combined.append(op).append(clause);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
combined.append(')');
|
|
172
|
+
parts.push(combined);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// RegExp
|
|
177
|
+
if (value instanceof RegExp) {
|
|
178
|
+
parts.push(new SQLStatement([`\`${key}\` REGEXP `], []).append(SQL`${value.source}`));
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Filter operators: {age: {$gte: 18}}
|
|
183
|
+
if (value && typeof value === 'object' && !(value instanceof Date)) {
|
|
184
|
+
if (Object.keys(value).length === 0) {
|
|
185
|
+
throw new Error('Cannot compare to empty object value');
|
|
186
|
+
}
|
|
187
|
+
parts.push(_filter(key, value as sql_types.Operator));
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Simple equality
|
|
192
|
+
parts.push(new SQLStatement([`\`${key}\` = `], []).append(SQL`${value}`));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Join with AND
|
|
196
|
+
if (parts.length === 0) {
|
|
197
|
+
return new SQLStatement([''], []);
|
|
198
|
+
}
|
|
199
|
+
let result = parts[0]!;
|
|
200
|
+
for (let i = 1; i < parts.length; i++) {
|
|
201
|
+
const part = parts[i];
|
|
202
|
+
if (part) {
|
|
203
|
+
result.append(' AND ').append(part);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Build filter operators like {$gte: 18, $lte: 65}
|
|
211
|
+
*/
|
|
212
|
+
function _filter(column: string, operator: sql_types.Operator): SQLStatement {
|
|
213
|
+
const parts: SQLStatement[] = [];
|
|
214
|
+
|
|
215
|
+
for (const [key, value] of Object.entries(operator)) {
|
|
216
|
+
switch (key) {
|
|
217
|
+
case '$eq':
|
|
218
|
+
parts.push(new SQLStatement([`\`${column}\` = `], []).append(SQL`${value}`));
|
|
219
|
+
break;
|
|
220
|
+
case '$gt':
|
|
221
|
+
parts.push(new SQLStatement([`\`${column}\` > `], []).append(SQL`${value}`));
|
|
222
|
+
break;
|
|
223
|
+
case '$gte':
|
|
224
|
+
parts.push(new SQLStatement([`\`${column}\` >= `], []).append(SQL`${value}`));
|
|
225
|
+
break;
|
|
226
|
+
case '$lt':
|
|
227
|
+
parts.push(new SQLStatement([`\`${column}\` < `], []).append(SQL`${value}`));
|
|
228
|
+
break;
|
|
229
|
+
case '$lte':
|
|
230
|
+
parts.push(new SQLStatement([`\`${column}\` <= `], []).append(SQL`${value}`));
|
|
231
|
+
break;
|
|
232
|
+
case '$ne':
|
|
233
|
+
parts.push(new SQLStatement([`\`${column}\` <> `], []).append(SQL`${value}`));
|
|
234
|
+
break;
|
|
235
|
+
case '$in':
|
|
236
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
237
|
+
let stmt = new SQLStatement([`\`${column}\` IN (`], []);
|
|
238
|
+
stmt.append(SQL`${value[0]}`);
|
|
239
|
+
for (let i = 1; i < value.length; i++) {
|
|
240
|
+
stmt.append(', ').append(SQL`${value[i]}`);
|
|
241
|
+
}
|
|
242
|
+
stmt.append(')');
|
|
243
|
+
parts.push(stmt);
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
246
|
+
case '$nin':
|
|
247
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
248
|
+
let stmt = new SQLStatement([`\`${column}\` NOT IN (`], []);
|
|
249
|
+
stmt.append(SQL`${value[0]}`);
|
|
250
|
+
for (let i = 1; i < value.length; i++) {
|
|
251
|
+
stmt.append(', ').append(SQL`${value[i]}`);
|
|
252
|
+
}
|
|
253
|
+
stmt.append(')');
|
|
254
|
+
parts.push(stmt);
|
|
255
|
+
}
|
|
256
|
+
break;
|
|
257
|
+
case '$exists':
|
|
258
|
+
const suffix = value === true ? ' IS NOT NULL' : ' IS NULL';
|
|
259
|
+
parts.push(new SQLStatement([`\`${column}\`${suffix}`], []));
|
|
260
|
+
break;
|
|
261
|
+
case '$not':
|
|
262
|
+
const notClause = _filter(column, value);
|
|
263
|
+
parts.push(new SQLStatement([`\`${column}\` NOT (`], []).append(notClause).append(')'));
|
|
264
|
+
break;
|
|
265
|
+
default:
|
|
266
|
+
throw new Error(`Invalid filter operator: ${key}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Join with AND
|
|
271
|
+
if (parts.length === 0) {
|
|
272
|
+
return new SQLStatement([''], []);
|
|
273
|
+
}
|
|
274
|
+
let result = parts[0]!;
|
|
275
|
+
for (let i = 1; i < parts.length; i++) {
|
|
276
|
+
const part = parts[i];
|
|
277
|
+
if (part) {
|
|
278
|
+
result.append(' AND ').append(part);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Build ORDER BY clause (returns plain string - no params)
|
|
286
|
+
*/
|
|
287
|
+
function _orderBy(order: sql_types.OrderBy): string {
|
|
288
|
+
const VALID_COLUMN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
289
|
+
const VALID_DIR = ['asc', 'desc'];
|
|
290
|
+
|
|
291
|
+
const parts: string[] = [];
|
|
292
|
+
for (const [col, dir] of Object.entries(order)) {
|
|
293
|
+
if (!VALID_COLUMN.test(col)) {
|
|
294
|
+
throw new Error(`Invalid column name in ORDER BY: "${col}"`);
|
|
295
|
+
}
|
|
296
|
+
const normalized = dir.toLowerCase();
|
|
297
|
+
if (!VALID_DIR.includes(normalized)) {
|
|
298
|
+
throw new Error(`Invalid direction in ORDER BY: "${dir}"`);
|
|
299
|
+
}
|
|
300
|
+
parts.push(`\`${col}\` ${normalized.toUpperCase()}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return ` ORDER BY ${parts.join(', ')}`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Build LIMIT clause (returns plain string - no params)
|
|
308
|
+
*/
|
|
309
|
+
function _limit(limit: string): string {
|
|
310
|
+
const pattern = /^(\d+)(?:\s*,\s*(\d+)|\s+OFFSET\s+(\d+))?$/i;
|
|
311
|
+
if (!pattern.test(limit)) {
|
|
312
|
+
throw new Error(`Invalid LIMIT clause: "${limit}"`);
|
|
313
|
+
}
|
|
314
|
+
return ` LIMIT ${limit}`;
|
|
315
|
+
}
|
package/.uranio/src/sql/index.ts
CHANGED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL template string builder
|
|
3
|
+
* Simplified implementation inspired by sql-template-strings
|
|
4
|
+
*
|
|
5
|
+
* This provides a secure way to build SQL queries using template literals.
|
|
6
|
+
* Values are never embedded into SQL strings, preventing SQL injection.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const name = 'John';
|
|
10
|
+
* const query = SQL`SELECT * FROM users WHERE name = ${name}`;
|
|
11
|
+
*
|
|
12
|
+
* // For MySQL:
|
|
13
|
+
* query.mysql() // { sql: "SELECT * FROM users WHERE name = ?", values: ['John'] }
|
|
14
|
+
*
|
|
15
|
+
* // For PostgreSQL:
|
|
16
|
+
* query.postgres() // { sql: "SELECT * FROM users WHERE name = $1", values: ['John'] }
|
|
17
|
+
*
|
|
18
|
+
* @packageDocumentation
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export class SQLStatement {
|
|
22
|
+
private strings: string[];
|
|
23
|
+
private values: any[];
|
|
24
|
+
|
|
25
|
+
constructor(strings: string[], values: any[]) {
|
|
26
|
+
this.strings = strings;
|
|
27
|
+
this.values = values;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns MySQL/MariaDB-compatible query with ? placeholders
|
|
32
|
+
*/
|
|
33
|
+
mysql(): { sql: string; values: any[] } {
|
|
34
|
+
return {
|
|
35
|
+
sql: this.strings.join('?'),
|
|
36
|
+
values: this.values,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns PostgreSQL-compatible query with $1, $2, ... placeholders
|
|
42
|
+
*/
|
|
43
|
+
postgres(): { sql: string; values: any[] } {
|
|
44
|
+
const sql = this.strings.reduce((prev, curr, i) => {
|
|
45
|
+
if (i === 0) return curr;
|
|
46
|
+
return prev + '$' + i + curr;
|
|
47
|
+
}, '');
|
|
48
|
+
return {
|
|
49
|
+
sql,
|
|
50
|
+
values: this.values,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Append another SQL statement or string
|
|
56
|
+
*/
|
|
57
|
+
append(statement: SQLStatement | string): this {
|
|
58
|
+
if (statement instanceof SQLStatement) {
|
|
59
|
+
// Merge the last string of this with the first string of statement
|
|
60
|
+
const lastIdx = this.strings.length - 1;
|
|
61
|
+
const firstString = statement.strings[0] || '';
|
|
62
|
+
this.strings[lastIdx] = (this.strings[lastIdx] || '') + firstString;
|
|
63
|
+
this.strings.push(...statement.strings.slice(1));
|
|
64
|
+
this.values.push(...statement.values);
|
|
65
|
+
} else {
|
|
66
|
+
// Just append a string to the last segment
|
|
67
|
+
const lastIdx = this.strings.length - 1;
|
|
68
|
+
this.strings[lastIdx] = (this.strings[lastIdx] || '') + statement;
|
|
69
|
+
}
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get the raw values array
|
|
75
|
+
*/
|
|
76
|
+
getValues(): any[] {
|
|
77
|
+
return this.values;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get the number of parameters
|
|
82
|
+
*/
|
|
83
|
+
get paramCount(): number {
|
|
84
|
+
return this.values.length;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Tagged template function for SQL queries
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* const name = 'John';
|
|
93
|
+
* const age = 30;
|
|
94
|
+
* const query = SQL`SELECT * FROM users WHERE name = ${name} AND age = ${age}`;
|
|
95
|
+
*/
|
|
96
|
+
export function SQL(strings: TemplateStringsArray, ...values: any[]): SQLStatement {
|
|
97
|
+
return new SQLStatement(Array.from(strings), values);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default SQL;
|