@mlagie/sql-connector 3.0.2 → 3.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 +985 -316
- package/docs/API.md +240 -0
- package/docs/DIALECTS.md +56 -0
- package/docs/MIGRATIONS.md +277 -0
- package/docs/QUERY.md +281 -0
- package/docs/README.md +20 -0
- package/docs/SCHEMA.md +167 -0
- package/docs/fr/README_FR.md +549 -305
- package/index.d.ts +2002 -137
- package/index.js +39 -9
- package/package.json +7 -6
- package/src/db/dialects.js +1 -1
- package/src/migration/BackupManager.js +42 -0
- package/src/migration/Migration.js +163 -0
- package/src/migration/MigrationPlan.js +62 -0
- package/src/migration/MigrationRunner.js +194 -0
- package/src/migration/MigrationStore.js +137 -0
- package/src/migration/SchemaDiffer.js +261 -0
- package/src/migration/SchemaInspector.js +44 -0
- package/src/migration/SchemaSnapshot.js +53 -0
- package/src/migration/crypto.js +79 -0
- package/src/migration/dialects/Dialect.js +112 -0
- package/src/migration/dialects/MySQLDialect.js +205 -0
- package/src/migration/dialects/PostgreSQLDialect.js +233 -0
- package/src/migration/dialects/index.js +21 -0
- package/src/migration/errors.js +28 -0
- package/src/migration/index.js +18 -0
- package/src/migration/utils.js +89 -0
- package/src/models/Model.js +8 -2
- package/src/models/ModelInstance.js +3 -3
- package/src/models/Schema.js +20 -20
- package/src/utils/buildQuery/buildQuery.js +58 -17
- package/src/utils/sqlTypeMap.js +8 -0
package/src/models/Schema.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class Schema {
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Represents a database schema.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* const transferSchema = new Schema({
|
|
6
|
+
* token: {
|
|
7
|
+
* type: String,
|
|
8
|
+
* length: 50
|
|
9
|
+
* },
|
|
10
|
+
* mdp: {
|
|
11
|
+
* type: String,
|
|
12
|
+
* length: 15
|
|
13
|
+
* }
|
|
14
|
+
* });
|
|
15
|
+
*/
|
|
16
|
+
class Schema {
|
|
17
|
+
constructor(schemaDict) {
|
|
18
|
+
this.schemaDict = schemaDict;
|
|
19
|
+
}
|
|
19
20
|
}
|
|
20
|
-
}
|
|
21
21
|
|
|
22
|
-
module.exports = { Schema }
|
|
22
|
+
module.exports = { Schema }
|
|
@@ -61,7 +61,14 @@ function buildSelect(select = []) {
|
|
|
61
61
|
.join(',\n');
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
const COMPARISON_OPERATORS = ["=", "!=", ">", "<", ">=", "<=", "LIKE", "NOT LIKE", "ILIKE", "NOT ILIKE", "IN", "NOT IN", "BETWEEN", "NOT BETWEEN"];
|
|
65
|
+
|
|
66
|
+
function buildCaseInsensitiveLike(dialect, key, placeholder, negate) {
|
|
67
|
+
if (dialect.name === "postgres") return null;
|
|
68
|
+
return `${negate ? "NOT " : ""}LOWER(${dialect.escape(key)}) LIKE LOWER(${placeholder})`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildWhere(where, values, offset) {
|
|
65
72
|
const conditions = [];
|
|
66
73
|
|
|
67
74
|
for (const [key, value] of Object.entries(where)) {
|
|
@@ -70,7 +77,7 @@ function buildWhere(where, values) {
|
|
|
70
77
|
throw new Error("OR conditions cannot be empty");
|
|
71
78
|
}
|
|
72
79
|
const clauses = value
|
|
73
|
-
.map((item) => buildWhere(item, values))
|
|
80
|
+
.map((item) => buildWhere(item, values, offset))
|
|
74
81
|
.filter(Boolean);
|
|
75
82
|
|
|
76
83
|
if (clauses.length) {
|
|
@@ -85,7 +92,7 @@ function buildWhere(where, values) {
|
|
|
85
92
|
throw new Error("AND conditions cannot be empty");
|
|
86
93
|
}
|
|
87
94
|
const clauses = value
|
|
88
|
-
.map((item) => buildWhere(item, values))
|
|
95
|
+
.map((item) => buildWhere(item, values, offset))
|
|
89
96
|
.filter(Boolean);
|
|
90
97
|
|
|
91
98
|
if (clauses.length) {
|
|
@@ -95,28 +102,49 @@ function buildWhere(where, values) {
|
|
|
95
102
|
continue;
|
|
96
103
|
}
|
|
97
104
|
|
|
105
|
+
if (key === "NOT") {
|
|
106
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
107
|
+
throw new Error("NOT condition must be an object");
|
|
108
|
+
}
|
|
109
|
+
const clause = buildWhere(value, values, offset);
|
|
110
|
+
if (clause) conditions.push(`NOT (${clause})`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
98
114
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
99
115
|
const operators = Object.keys(value);
|
|
100
116
|
|
|
101
|
-
const isOperatorObject = operators.some((op) =>
|
|
102
|
-
["=", "!=", ">", "<", ">=", "<=", "LIKE", "IN", "NOT IN"].includes(op)
|
|
103
|
-
);
|
|
117
|
+
const isOperatorObject = operators.some((op) => COMPARISON_OPERATORS.includes(op));
|
|
104
118
|
|
|
105
119
|
if (isOperatorObject) {
|
|
120
|
+
const dialect = getDialect();
|
|
121
|
+
|
|
106
122
|
for (const [operator, operatorValue] of Object.entries(value)) {
|
|
107
123
|
if (operator === "IN" || operator === "NOT IN") {
|
|
108
124
|
const placeholders = operatorValue
|
|
109
|
-
.map((_, i) =>
|
|
125
|
+
.map((_, i) => dialect.getPlaceholder(offset + values.length + i))
|
|
110
126
|
.join(",");
|
|
111
127
|
|
|
112
|
-
conditions.push(
|
|
113
|
-
`${getDialect().escape(key)} ${operator} (${placeholders})`
|
|
114
|
-
);
|
|
128
|
+
conditions.push(`${dialect.escape(key)} ${operator} (${placeholders})`);
|
|
115
129
|
values.push(...operatorValue);
|
|
130
|
+
} else if (operator === "BETWEEN" || operator === "NOT BETWEEN") {
|
|
131
|
+
const [min, max] = operatorValue;
|
|
132
|
+
const lowPlaceholder = dialect.getPlaceholder(offset + values.length);
|
|
133
|
+
const highPlaceholder = dialect.getPlaceholder(offset + values.length + 1);
|
|
134
|
+
|
|
135
|
+
conditions.push(`${dialect.escape(key)} ${operator} ${lowPlaceholder} AND ${highPlaceholder}`);
|
|
136
|
+
values.push(min, max);
|
|
137
|
+
} else if (operator === "ILIKE" || operator === "NOT ILIKE") {
|
|
138
|
+
const placeholder = dialect.getPlaceholder(offset + values.length);
|
|
139
|
+
const negate = operator === "NOT ILIKE";
|
|
140
|
+
const fallback = buildCaseInsensitiveLike(dialect, key, placeholder, negate);
|
|
141
|
+
|
|
142
|
+
conditions.push(fallback ?? `${dialect.escape(key)} ${operator} ${placeholder}`);
|
|
143
|
+
values.push(operatorValue);
|
|
116
144
|
} else if (operatorValue === null && (operator === "=" || operator === "!=")) {
|
|
117
|
-
conditions.push(`${
|
|
145
|
+
conditions.push(`${dialect.escape(key)} IS ${operator === "!=" ? "NOT " : ""}NULL`);
|
|
118
146
|
} else {
|
|
119
|
-
conditions.push(`${
|
|
147
|
+
conditions.push(`${dialect.escape(key)} ${operator} ${dialect.getPlaceholder(offset + values.length)}`);
|
|
120
148
|
values.push(operatorValue);
|
|
121
149
|
}
|
|
122
150
|
}
|
|
@@ -129,7 +157,7 @@ function buildWhere(where, values) {
|
|
|
129
157
|
continue;
|
|
130
158
|
}
|
|
131
159
|
|
|
132
|
-
conditions.push(`${getDialect().escape(key)} = ${getDialect().getPlaceholder(values.length)}`);
|
|
160
|
+
conditions.push(`${getDialect().escape(key)} = ${getDialect().getPlaceholder(offset + values.length)}`);
|
|
133
161
|
values.push(value);
|
|
134
162
|
}
|
|
135
163
|
|
|
@@ -139,9 +167,10 @@ function buildWhere(where, values) {
|
|
|
139
167
|
/**
|
|
140
168
|
* Construit les parties de la requête et extrait les valeurs sécurisées
|
|
141
169
|
* @param {Object} options Options de filtrage, tri et pagination
|
|
170
|
+
* @param {number} [valueOffset=0] Décalage d'index pour les placeholders (utile quand des valeurs ont déjà été liées avant le WHERE, ex: SET d'un UPDATE)
|
|
142
171
|
* @returns {Object} Un objet contenant la chaîne SQL générée et le tableau des valeurs { sql, values }
|
|
143
172
|
*/
|
|
144
|
-
function buildQueryParts(options) {
|
|
173
|
+
function buildQueryParts(options, valueOffset = 0) {
|
|
145
174
|
const parts = [];
|
|
146
175
|
const values = [];
|
|
147
176
|
|
|
@@ -152,7 +181,7 @@ function buildQueryParts(options) {
|
|
|
152
181
|
if (typeof options.where !== 'object' || Array.isArray(options.where)) {
|
|
153
182
|
throw new Error("Raw string WHERE clauses are not allowed. Use a structured filter instead.");
|
|
154
183
|
}
|
|
155
|
-
parts.push(`WHERE ${buildWhere(options.where, values)}`);
|
|
184
|
+
parts.push(`WHERE ${buildWhere(options.where, values, valueOffset)}`);
|
|
156
185
|
}
|
|
157
186
|
|
|
158
187
|
if (options.groupBy) {
|
|
@@ -160,7 +189,10 @@ function buildQueryParts(options) {
|
|
|
160
189
|
}
|
|
161
190
|
|
|
162
191
|
if (options.having) {
|
|
163
|
-
|
|
192
|
+
if (typeof options.having !== 'object' || Array.isArray(options.having)) {
|
|
193
|
+
throw new Error("Raw string HAVING clauses are not allowed. Use a structured filter instead.");
|
|
194
|
+
}
|
|
195
|
+
parts.push(`HAVING ${buildWhere(options.having, values, valueOffset)}`);
|
|
164
196
|
}
|
|
165
197
|
|
|
166
198
|
if (options.orderBy) {
|
|
@@ -177,10 +209,19 @@ function buildQueryParts(options) {
|
|
|
177
209
|
throw new Error("Invalid LIMIT value");
|
|
178
210
|
}
|
|
179
211
|
|
|
180
|
-
parts.push(`LIMIT
|
|
212
|
+
parts.push(`LIMIT ${getDialect().getPlaceholder(valueOffset + values.length)}`);
|
|
181
213
|
values.push(options.limit);
|
|
182
214
|
}
|
|
183
215
|
|
|
216
|
+
if (options.offset !== undefined) {
|
|
217
|
+
if (!Number.isInteger(options.offset) || options.offset < 0) {
|
|
218
|
+
throw new Error("Invalid OFFSET value");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
parts.push(`OFFSET ${getDialect().getPlaceholder(valueOffset + values.length)}`);
|
|
222
|
+
values.push(options.offset);
|
|
223
|
+
}
|
|
224
|
+
|
|
184
225
|
return {
|
|
185
226
|
sql: parts.join('\n\n'),
|
|
186
227
|
values: values
|
package/src/utils/sqlTypeMap.js
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
const sqlTypeMap = {
|
|
2
2
|
String: 'VARCHAR',
|
|
3
|
+
Char: 'CHAR',
|
|
3
4
|
Number: 'INT',
|
|
5
|
+
SmallInt: 'SMALLINT',
|
|
6
|
+
BigInt: 'BIGINT',
|
|
7
|
+
Decimal: 'DECIMAL',
|
|
4
8
|
Boolean: 'BOOLEAN',
|
|
5
9
|
Date: 'DATETIME',
|
|
6
10
|
Object: 'JSON',
|
|
7
11
|
Array: 'VARCHAR',
|
|
8
12
|
Now: 'NOW()',
|
|
9
13
|
Float: 'FLOAT',
|
|
14
|
+
Double: 'DOUBLE',
|
|
10
15
|
Text: 'TEXT',
|
|
16
|
+
Blob: 'BLOB',
|
|
17
|
+
Binary: 'VARBINARY',
|
|
18
|
+
Uuid: 'CHAR',
|
|
11
19
|
DateTime: "DATETIME",
|
|
12
20
|
Timestamp: "TIMESTAMP",
|
|
13
21
|
CurrentTimestamp: "CURRENT_TIMESTAMP"
|