@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.
@@ -1,22 +1,22 @@
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;
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
- function buildWhere(where, values) {
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) => getDialect().getPlaceholder(values.length + 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(`${getDialect().escape(key)} IS ${operator === "!=" ? "NOT " : ""}NULL`);
145
+ conditions.push(`${dialect.escape(key)} IS ${operator === "!=" ? "NOT " : ""}NULL`);
118
146
  } else {
119
- conditions.push(`${getDialect().escape(key)} ${operator} ${getDialect().getPlaceholder(values.length)}`);
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
- throw new Error("Raw string HAVING clauses are not allowed. Use a structured filter instead.");
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
@@ -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"