@mlagie/sql-connector 2.0.5 → 2.0.7
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/.github/workflows/publish.yml +35 -25
- package/README.md +107 -14
- package/docs/fr/README.md +114 -19
- package/package.json +8 -3
- package/releases/2.0.6.md +80 -0
- package/releases/2.0.7.md +38 -0
- package/socket.yml +4 -0
- package/src/models/Model.js +35 -33
- package/src/models/ModelInstance.js +19 -28
- package/src/utils/{buildQuery.js → buildQuery/buildQuery.js} +6 -6
- package/src/utils/buildQuery/count.js +11 -0
- package/src/utils/formatObject.js +1 -0
- package/src/utils/generateCondition.js +0 -8
- package/tests/Model.test.js +666 -0
- package/tests/ModelInstance.test.js +335 -0
- package/tests/buildQuery.test.js +134 -19
- package/tests/connect.test.js +14 -4
- package/tests/formatObject.test.js +9 -0
- package/tests/generateCondition.test.js +200 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
name: Publish Package
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
|
+
workflow_dispatch:
|
|
4
5
|
release:
|
|
5
6
|
types: [published]
|
|
6
7
|
push:
|
|
@@ -9,7 +10,6 @@ on:
|
|
|
9
10
|
pull_request:
|
|
10
11
|
branches:
|
|
11
12
|
- main
|
|
12
|
-
workflow_dispatch:
|
|
13
13
|
jobs:
|
|
14
14
|
check_security:
|
|
15
15
|
runs-on: ubuntu-latest
|
|
@@ -33,38 +33,48 @@ jobs:
|
|
|
33
33
|
integrity:
|
|
34
34
|
runs-on: ubuntu-latest
|
|
35
35
|
steps:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
36
|
+
- name: Checkout GH repository
|
|
37
|
+
uses: actions/checkout@v6
|
|
38
|
+
- name: setup node
|
|
39
|
+
uses: actions/setup-node@v6
|
|
40
|
+
with:
|
|
41
|
+
node-version: 22
|
|
42
|
+
- name: Install deps
|
|
43
|
+
run: npm ci
|
|
44
|
+
- name: Run integrity check
|
|
45
|
+
continue-on-error: false
|
|
46
|
+
run: |
|
|
47
|
+
npm run test
|
|
48
48
|
publish:
|
|
49
|
-
needs:
|
|
49
|
+
needs:
|
|
50
50
|
- check_security
|
|
51
51
|
- integrity
|
|
52
|
-
if: github.event_name == 'release' || github.
|
|
52
|
+
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
|
|
53
53
|
runs-on: ubuntu-latest
|
|
54
54
|
environment: sqlc
|
|
55
55
|
permissions:
|
|
56
56
|
contents: read
|
|
57
|
-
id-token: write
|
|
57
|
+
id-token: write # Requis pour la provenance (--provenance)
|
|
58
58
|
|
|
59
59
|
steps:
|
|
60
|
-
|
|
61
|
-
-
|
|
60
|
+
# 1. Récupérer le code source
|
|
61
|
+
- name: Checkout du code
|
|
62
|
+
uses: actions/checkout@v6
|
|
63
|
+
|
|
64
|
+
# 2. Configurer Node.js pour registry.npmjs.org
|
|
65
|
+
- name: Configurer Node.js
|
|
66
|
+
uses: actions/setup-node@v6
|
|
62
67
|
with:
|
|
63
|
-
node-version: 22
|
|
64
|
-
registry-url: https://registry.npmjs.org
|
|
68
|
+
node-version: "22"
|
|
69
|
+
registry-url: "https://registry.npmjs.org/" # <-- Cible npmjs
|
|
65
70
|
scope: "@mlagie"
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
|
|
72
|
+
# 3. Installer les dépendances
|
|
73
|
+
- name: Installer les dépendances
|
|
74
|
+
run: npm ci
|
|
75
|
+
|
|
76
|
+
# 4. Publier sur le vrai registre npm
|
|
77
|
+
- name: Publier sur npmjs.com
|
|
78
|
+
run: npm publish --provenance --access public
|
|
79
|
+
env:
|
|
80
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/README.md
CHANGED
|
@@ -184,23 +184,51 @@ async function createUser(email, stat) {
|
|
|
184
184
|
|
|
185
185
|
Retrieves entries from the table.
|
|
186
186
|
|
|
187
|
-
- **Parameters** `options` *(Object)* - Query options
|
|
188
|
-
- **Parameters** `options.select` *(string
|
|
189
|
-
- **Parameters** `options.where` *(Object)* -
|
|
190
|
-
- **Parameters** `options.
|
|
191
|
-
- **Parameters** `options.
|
|
187
|
+
- **Parameters** `options` *(Object)* - Query options.
|
|
188
|
+
- **Parameters** `options.select` *(Array<string|SelectAggregation>)* - Fields, aggregations, or transformations to be returned.
|
|
189
|
+
- **Parameters** `options.where` *(Object / string)* - Filtering conditions (key/value object or raw string condition).
|
|
190
|
+
- **Parameters** `options.groupBy` *(string[])* - Fields used to group results.
|
|
191
|
+
- **Parameters** `options.orderBy` *(Array<string|OrderByOption>)* - Sorting rules.
|
|
192
|
+
- **Parameters** `options.join` *(JoinOption / JoinOption[])* - Table join configuration structures.
|
|
193
|
+
- **Parameters** `options.limit` *(number)* - Maximum number of results to return.
|
|
192
194
|
- **Returns** `Promise<Array<ModelInstance>>`
|
|
193
195
|
|
|
194
|
-
###
|
|
196
|
+
### Advanced `select` Options (`SelectAggregation`)
|
|
195
197
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
| `select`
|
|
199
|
-
|
|
200
|
-
| `
|
|
201
|
-
| `
|
|
202
|
-
| `
|
|
203
|
-
| `
|
|
198
|
+
Each element in the `select` array can be either a standard string (raw column name) or an object providing advanced SQL capabilities and aggregations:
|
|
199
|
+
|
|
200
|
+
| Property inside `select` object | Type | Description | Example / Generated SQL |
|
|
201
|
+
|---------------------------------|-------------------|------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------|
|
|
202
|
+
| `col` | `string` | Selects a plain, unaggregated table column. | `{ col: 'email' }` $\rightarrow$ \`email\` |
|
|
203
|
+
| `sum` | `string` | Computes the sum of all numerical values in a column. | `{ sum: 'error' }` $\rightarrow$ `SUM(` \`error\``)` |
|
|
204
|
+
| `distinct` | `string` | Applies a DISTINCT constraint on the specified column. | `{ distinct: 'status' }` $\rightarrow$ `DISTINCT` \`status\` |
|
|
205
|
+
| `dateFormat` | `[string, string]`| Formats a Date column using standard MySQL formatting (Format: `[column, mysql_format_string]`). | `{ dateFormat: ['created_at', '%Y-%m'] }` $\rightarrow$ `DATE_FORMAT(` \`created_at\``, '%Y-%m')` |
|
|
206
|
+
| `count` | `string` | Standard row counting (ignores `NULL` values). | `{ count: 'id' }` $\rightarrow$ `COUNT(` \`id\``)` |
|
|
207
|
+
| `count` (Array) | `string[]` | Counts unique combinations across multiple columns (COUNT DISTINCT). | `{ count: ['team', 'source'] }` $\rightarrow$ `COUNT( DISTINCT` \`team\``,` \`source\``)` |
|
|
208
|
+
| `count` (Object) | `Object` | Automated conditional aggregation (`CASE WHEN`). Perfect for KPIs and status metrics. | `{ count: { deletedAt: null } }` $\rightarrow$ `COUNT(CASE WHEN` \`deletedAt\``= NULL THEN 1 END)` |
|
|
209
|
+
| `as` | `string` | Sets a custom output identifier or aggregation alias (SQL `AS`). | `{ count: 'id', as: 'total' }` $\rightarrow$ `COUNT(` \`id\``) AS` \`total\` |
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
### Complex Structured Options (`orderBy` & `join`)
|
|
214
|
+
|
|
215
|
+
#### OrderByOption
|
|
216
|
+
|
|
217
|
+
Enables explicit sorting across one or multiple columns:
|
|
218
|
+
|
|
219
|
+
- **field** `(string)`: The target column name to apply sorting on.
|
|
220
|
+
- **direction** `('ASC'\|'DESC')`: The sorting direction (Defaults to `'ASC'`).
|
|
221
|
+
|
|
222
|
+
#### JoinOption
|
|
223
|
+
|
|
224
|
+
Specifies one or multiple relational database table joins:
|
|
225
|
+
|
|
226
|
+
- **table** `(string)`: Target table name to join with.
|
|
227
|
+
- **on** `(string)`: Relational equation statement string (e.g., `"ProjectPipelines.project_id = Projects.id"`).
|
|
228
|
+
- **alias** `(string)` *(Optional)*: An alternative SQL alias name for the joined table.
|
|
229
|
+
- **type** `('INNER'\|'LEFT'\|'RIGHT')` *(Optional)*: SQL join modality (Defaults to `'INNER'`).
|
|
230
|
+
|
|
231
|
+
---
|
|
204
232
|
|
|
205
233
|
## Example find
|
|
206
234
|
|
|
@@ -232,6 +260,71 @@ User.find({
|
|
|
232
260
|
})
|
|
233
261
|
```
|
|
234
262
|
|
|
263
|
+
### Advanced Examples using `find`
|
|
264
|
+
|
|
265
|
+
#### 1. Standard, Distinct, and Multi-Column Counting
|
|
266
|
+
|
|
267
|
+
Count global rows alongside multi-column unique combinations, such as identifying unique team and source pipelines:
|
|
268
|
+
|
|
269
|
+
```js
|
|
270
|
+
const { MyTable } = require("./models");
|
|
271
|
+
|
|
272
|
+
const stats = await MyTable.find({
|
|
273
|
+
select: [
|
|
274
|
+
{ count: 'id', as: 'total_pipelines' },
|
|
275
|
+
{ count: ['cteam', 'csource'], as: 'unique_groups' } // Multi-column COUNT DISTINCT
|
|
276
|
+
],
|
|
277
|
+
where: { csource: 'web' }
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
#### 2. Conditional Aggregations (Active / Decommissioned KPI Metrics)
|
|
282
|
+
|
|
283
|
+
By passing an object to the `count` attribute, the ORM automatically structures a conditional `CASE WHEN` clause. This allows you to split different status counters into a single database trip:
|
|
284
|
+
|
|
285
|
+
```js
|
|
286
|
+
const { MyTable } = require("./models");
|
|
287
|
+
|
|
288
|
+
const ppiStats = await MyTable.find({
|
|
289
|
+
select: [
|
|
290
|
+
{ count: { deletedAt: null }, as: 'active' }, // Counts where deletedAt = NULL
|
|
291
|
+
{ count: { status: 'SUCCESS' }, as: 'total_success' } // Counts where status = 'SUCCESS'
|
|
292
|
+
],
|
|
293
|
+
where: {
|
|
294
|
+
source: 'jenkins',
|
|
295
|
+
team: 'GROUP-1'
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
#### 3. Table Joins, Time Series Grouping, and Multi-Column Sorting
|
|
301
|
+
|
|
302
|
+
An advanced query orchestration combining left table joining, date formatting conversions, and sorting:
|
|
303
|
+
|
|
304
|
+
```js
|
|
305
|
+
const { MyTable } = require("./models");
|
|
306
|
+
|
|
307
|
+
const history = await MyTable.find({
|
|
308
|
+
select: [
|
|
309
|
+
{ col: 'Projects.name', as: 'project_name' },
|
|
310
|
+
{ dateFormat: ['ProjectPipelines.created_at', '%Y-%m'], as: 'period' },
|
|
311
|
+
{ count: 'ProjectPipelines.id', as: 'pipelines_count' }
|
|
312
|
+
],
|
|
313
|
+
where: "ProjectPipelines.deletedAt IS NULL", // Raw condition strings are permitted
|
|
314
|
+
join: {
|
|
315
|
+
table: 'Projects',
|
|
316
|
+
on: 'ProjectPipelines.project_id = Projects.id',
|
|
317
|
+
type: 'LEFT'
|
|
318
|
+
},
|
|
319
|
+
groupBy: ['project_name', 'period'],
|
|
320
|
+
orderBy: [
|
|
321
|
+
{ field: 'period', direction: 'DESC' },
|
|
322
|
+
{ field: 'project_name', direction: 'ASC' }
|
|
323
|
+
],
|
|
324
|
+
limit: 50
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
235
328
|
## count function
|
|
236
329
|
|
|
237
330
|
Counts the number of records matching the given filter.
|
package/docs/fr/README.md
CHANGED
|
@@ -147,25 +147,53 @@ async function createUser(email, stat) {
|
|
|
147
147
|
|
|
148
148
|
## Fonction find
|
|
149
149
|
|
|
150
|
-
Récupère des
|
|
151
|
-
|
|
152
|
-
- **
|
|
153
|
-
- **
|
|
154
|
-
- **
|
|
155
|
-
- **
|
|
156
|
-
- **
|
|
157
|
-
- **
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
| `
|
|
166
|
-
|
|
167
|
-
| `
|
|
168
|
-
| `
|
|
150
|
+
Récupère des enregistrements de la table.
|
|
151
|
+
|
|
152
|
+
- **Paramètres** `options` *(Object)* – Options de la requête.
|
|
153
|
+
- **Paramètres** `options.select` *(Array<string|SelectAggregation>)* – Champs, agrégations ou transformations à retourner.
|
|
154
|
+
- **Paramètres** `options.where` *(Object / string)* – Conditions de filtrage (objet clé/valeur ou clause brute sous forme de chaîne).
|
|
155
|
+
- **Paramètres** `options.groupBy` *(string[])* – Champs utilisés pour grouper les résultats.
|
|
156
|
+
- **Paramètres** `options.orderBy` *(Array<string|OrderByOption>)* – Règles de tri.
|
|
157
|
+
- **Paramètres** `options.join` *(JoinOption / JoinOption[])* – Structures de configuration pour les jointures de tables.
|
|
158
|
+
- **Paramètres** `options.limit` *(number)* – Nombre maximal de résultats à retourner.
|
|
159
|
+
- **Retourne** `Promise<Array<ModelInstance>>`
|
|
160
|
+
|
|
161
|
+
### Options avancées de `select` (`SelectAggregation`)
|
|
162
|
+
|
|
163
|
+
Chaque élément du tableau `select` peut être soit une chaîne de caractères standard (nom brut de la colonne), soit un objet offrant des fonctionnalités SQL et des agrégations avancées :
|
|
164
|
+
|
|
165
|
+
| Propriété dans l'objet `select` | Type | Description | Exemple / SQL Généré |
|
|
166
|
+
|---------------------------------|-------------------|-----------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|
|
|
167
|
+
| `col` | `string` | Sélectionne une colonne de table simple, sans agrégation. | `{ col: 'email' }` $\rightarrow$ \`email\` |
|
|
168
|
+
| `sum` | `string` | Calcule la somme de toutes les valeurs numériques d'une colonne. | `{ sum: 'error' }` $\rightarrow$ `SUM(`\`error\``)` |
|
|
169
|
+
| `distinct` | `string` | Applique une contrainte DISTINCT sur la colonne spécifiée. | `{ distinct: 'status' }` $\rightarrow$ `DISTINCT `\`status\` |
|
|
170
|
+
| `dateFormat` | `[string, string]`| Formate une colonne de type Date en utilisant le formatage standard MySQL (`[colonne, chaine_de_format]`).| `{ dateFormat: ['created_at', '%Y-%m'] }` $\rightarrow$ `DATE_FORMAT(`\`created_at\``, '%Y-%m')` |
|
|
171
|
+
| `count` | `string` | Comptage de lignes standard (ignore les valeurs `NULL`). | `{ count: 'id' }` $\rightarrow$ `COUNT(`\`id\``)` |
|
|
172
|
+
| `count` (Array) | `string[]` | Calcule le nombre de combinaisons uniques sur plusieurs colonnes (COUNT DISTINCT). | `{ count: ['team', 'source'] }` $\rightarrow$ `COUNT( DISTINCT `\`team\``, `\`source\`` )` |
|
|
173
|
+
| `count` (Object) | `Object` | Agrégation conditionnelle automatisée (`CASE WHEN`). Idéal pour les indicateurs clés (KPIs) et statuts. | `{ count: { deletedAt: null } }` $\rightarrow$ `COUNT(CASE WHEN `\`deletedAt\`` = NULL THEN 1 END)` |
|
|
174
|
+
| `as` | `string` | Définit un identifiant de sortie personnalisé ou un alias d'agrégation (SQL `AS`). | `{ count: 'id', as: 'total' }` $\rightarrow$ `COUNT(`\`id\``) AS `\`total\` |
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
### Options complexes structurées (`orderBy` & `join`)
|
|
179
|
+
|
|
180
|
+
#### OrderByOption
|
|
181
|
+
|
|
182
|
+
Permet d'appliquer un tri explicite sur une ou plusieurs colonnes :
|
|
183
|
+
|
|
184
|
+
- **field** `(string)` : Le nom de la colonne cible sur laquelle appliquer le tri.
|
|
185
|
+
- **direction** `('ASC'\|'DESC')` : Le sens du tri (Par défaut : `'ASC'`).
|
|
186
|
+
|
|
187
|
+
#### JoinOption
|
|
188
|
+
|
|
189
|
+
Spécifie une ou plusieurs jointures de tables relationnelles :
|
|
190
|
+
|
|
191
|
+
- **table** `(string)` : Nom de la table cible à joindre.
|
|
192
|
+
- **on** `(string)` : Chaîne de caractères représentant la condition de jointure (ex: `"MyTable.project_id = Projects.id"`).
|
|
193
|
+
- **alias** `(string)` *(Optionnel)* : Un alias SQL alternatif pour la table jointe.
|
|
194
|
+
- **type** `('INNER'\|'LEFT'\|'RIGHT')` *(Optionnel)* : Type de jointure SQL (Par défaut : `'INNER'`).
|
|
195
|
+
|
|
196
|
+
---
|
|
169
197
|
|
|
170
198
|
## Exemple find
|
|
171
199
|
|
|
@@ -197,6 +225,73 @@ await User.find({
|
|
|
197
225
|
})
|
|
198
226
|
```
|
|
199
227
|
|
|
228
|
+
### Exemples avancés avec `find`
|
|
229
|
+
|
|
230
|
+
#### 1. Comptage standard, distinct et multi-colonnes
|
|
231
|
+
|
|
232
|
+
Comptez les lignes globales parallèlement à des combinaisons uniques multi-colonnes, comme l'identification des couples uniques d'équipes et de sources de pipelines :
|
|
233
|
+
|
|
234
|
+
```js
|
|
235
|
+
const { MyTable } = require("./models");
|
|
236
|
+
|
|
237
|
+
const stats = await MyTable.find({
|
|
238
|
+
select: [
|
|
239
|
+
{ count: 'id', as: 'total_pipelines' },
|
|
240
|
+
{ count: ['cteam', 'csource'], as: 'unique_groups' } // COUNT DISTINCT multi-colonnes
|
|
241
|
+
],
|
|
242
|
+
where: { csource: 'web' }
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
#### 2. Agrégations conditionnelles (Indicateurs KPIs Actifs / Inactifs)
|
|
247
|
+
|
|
248
|
+
En passant un objet à l'attribut count, l'ORM structure automatiquement une clause conditionnelle CASE WHEN. Cela vous permet de ventiler différents compteurs de statuts en une seule et unique requête vers la base de données :
|
|
249
|
+
|
|
250
|
+
```js
|
|
251
|
+
const { ProjectPipeline } = require("./models");
|
|
252
|
+
|
|
253
|
+
const ppiStats = await ProjectPipeline.find({
|
|
254
|
+
select: [
|
|
255
|
+
{ count: { deletedAt: null }, as: 'active' }, // Compte les lignes où deletedAt = NULL
|
|
256
|
+
{ count: { status: 'SUCCESS' }, as: 'total_success' } // Compte les lignes où status = 'SUCCESS'
|
|
257
|
+
],
|
|
258
|
+
where: {
|
|
259
|
+
source: 'jenkins',
|
|
260
|
+
team: 'GROUP-1'
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Format du tableau de sortie retourné : [{ active: 6, total_success: 42 }]
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
#### 3. Jointures de tables, groupement temporel et tri multi-colonnes
|
|
268
|
+
|
|
269
|
+
Une orchestration de requête avancée combinant une jointure gauche (LEFT JOIN), des conversions de formats de date et des tris :
|
|
270
|
+
|
|
271
|
+
```js
|
|
272
|
+
const { ProjectPipeline } = require("./models");
|
|
273
|
+
|
|
274
|
+
const history = await ProjectPipeline.find({
|
|
275
|
+
select: [
|
|
276
|
+
{ col: 'Projects.name', as: 'project_name' },
|
|
277
|
+
{ dateFormat: ['MyTable.created_at', '%Y-%m'], as: 'period' },
|
|
278
|
+
{ count: 'MyTable.id', as: 'pipelines_count' }
|
|
279
|
+
],
|
|
280
|
+
where: "MyTable.deletedAt IS NULL", // Les chaînes de conditions brutes sont autorisées
|
|
281
|
+
join: {
|
|
282
|
+
table: 'Projects',
|
|
283
|
+
on: 'MyTable.project_id = Projects.id',
|
|
284
|
+
type: 'LEFT'
|
|
285
|
+
},
|
|
286
|
+
groupBy: ['project_name', 'period'],
|
|
287
|
+
orderBy: [
|
|
288
|
+
{ field: 'period', direction: 'DESC' },
|
|
289
|
+
{ field: 'project_name', direction: 'ASC' }
|
|
290
|
+
],
|
|
291
|
+
limit: 50
|
|
292
|
+
});
|
|
293
|
+
```
|
|
294
|
+
|
|
200
295
|
## Fonction count
|
|
201
296
|
|
|
202
297
|
Compte le nombre d'enregistrements correspondant au filtre donné.
|
package/package.json
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mlagie/sql-connector",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.7",
|
|
4
4
|
"description": "Le module sql-connector permet de gérer les connexions à une base de données MySQL, de définir des schémas de tables, et d'interagir avec les données de manière simple et efficace.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand",
|
|
7
|
+
"test": "rm -r coverage ; node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand",
|
|
8
|
+
"test:publish": "npm publish --dry-run",
|
|
8
9
|
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
|
|
9
|
-
"test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
|
|
10
|
+
"test:coverage": "rm -r coverage ; node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
|
|
10
11
|
"security": "npx eslint . --max-warnings 0 --ignore-pattern './tests/*'"
|
|
11
12
|
},
|
|
12
13
|
"repository": {
|
|
13
14
|
"type": "git",
|
|
14
15
|
"url": "git+https://github.com/lagie-marin/sql-connector.git"
|
|
15
16
|
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"registry": "https://registry.npmjs.org/",
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
16
21
|
"keywords": [
|
|
17
22
|
"sql",
|
|
18
23
|
"schema",
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Release v2.0.6 — Architectural Property Isolation, Query Defusal & Test Coverage Expansion
|
|
2
|
+
|
|
3
|
+
This emergency structural release addresses a critical data-shadowing bug within the `ModelInstance` hydration cycle where active database column values (e.g., fields named `name`, `schema`, or `data`) overrode the core internal engine structures, mangling runtime query generation. It also introduces a comprehensive overhaul of the test suite to secure maximum coverage.
|
|
4
|
+
|
|
5
|
+
## 🛠️ The Bug Defusal (Why this was critical)
|
|
6
|
+
|
|
7
|
+
When `ModelInstance` mapped rows dynamically into getters/setters, a column named `name` from the database would mask `this.name` (the structural reference to the SQL Table Name).
|
|
8
|
+
|
|
9
|
+
As a result, an injection or ingestion update would mistakenly compile syntax failures like:
|
|
10
|
+
|
|
11
|
+
```sql
|
|
12
|
+
UPDATE my_value SET `deletedAt` = ... -- Mangled: Used row data instead of Table Name
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
By migrating all runtime configuration tokens to protected underscored properties (`_tableName`, `_data`, `_schema`), the instance context is now completely hermetic against database row schema payloads.
|
|
16
|
+
|
|
17
|
+
## Massive Test Coverage Expansion
|
|
18
|
+
|
|
19
|
+
To ensure the stability and robustness of this release, the unit test suites for both `Model` and `ModelInstance` have been significantly hardened:
|
|
20
|
+
|
|
21
|
+
- **Edge-Case & Fallback Testing**: Added specialized tests to force controlled failures in deep `try/catch` blocks, guaranteeing that the global engine fallback workflows function flawlessly under corrupted data scenarios.
|
|
22
|
+
|
|
23
|
+
- **SQL Mechanics Validation**: Built targeted unit tests covering schema dictionary mismatches, complex conditional mapping (e.g., table joins filtering mixed array types), and secondary logic paths inside `updateOne()`, `delete()`, and `deleteOne()`.
|
|
24
|
+
|
|
25
|
+
- `Database Simulation Coverage`: Implemented strict simulation sequences verifying UUID collision rejections (ensuring `generate_uuid()` handles duplicates properly) and missing response payloads (such as undefined `affectedRows`).
|
|
26
|
+
|
|
27
|
+
- `Zero-Crash Guarantees`: Hardened utility methods like `getFieldType()` against historical JavaScript edge cases (such as parsing `null` values safely).
|
|
28
|
+
|
|
29
|
+
## What Was Improved
|
|
30
|
+
|
|
31
|
+
- `Protected Context Isolation`: Migrated core metadata targets in `ModelInstance.js` to `_tableName`, `_data`, and `_schema`. Database columns can no longer shadow engine properties.
|
|
32
|
+
|
|
33
|
+
- `Tiret/Hyphen Database Safety`: Wrapped table calls explicitly in SQL backticks (\`) inside `updateOne`, `delete`, and `deleteOne` routines. Tables named with hyphens (like my_value) no longer trigger syntax violations.
|
|
34
|
+
|
|
35
|
+
- **Rehydration Mapping Consistency**: Standardized downstream sub-instantiations (like `customRequest`) to feed data strictly using the isolated `_tableName` descriptor.
|
|
36
|
+
|
|
37
|
+
## File Blueprint Updates
|
|
38
|
+
|
|
39
|
+
`ModelInstance.js`
|
|
40
|
+
|
|
41
|
+
The updated architecture encapsulates the internal core parameters like this:
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
class ModelInstance {
|
|
45
|
+
constructor(name, data, schema = null) {
|
|
46
|
+
Object.defineProperties(this, {
|
|
47
|
+
_tableName: { // 🔒 Fully protected from row data collisions
|
|
48
|
+
value: name,
|
|
49
|
+
writable: true,
|
|
50
|
+
configurable: true,
|
|
51
|
+
enumerable: false
|
|
52
|
+
},
|
|
53
|
+
_data: {
|
|
54
|
+
value: data,
|
|
55
|
+
writable: true,
|
|
56
|
+
configurable: true,
|
|
57
|
+
enumerable: true
|
|
58
|
+
},
|
|
59
|
+
_schema: {
|
|
60
|
+
value: schema,
|
|
61
|
+
writable: true,
|
|
62
|
+
configurable: true,
|
|
63
|
+
enumerable: false
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Dynamic row mapping safely bound without property leakage...
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async updateOne(model) {
|
|
71
|
+
const setClause = generateCondition(formatObject(model), true);
|
|
72
|
+
let whereClause = /* ... evaluation matrix using this._schema & this.getRecordData() ... */;
|
|
73
|
+
|
|
74
|
+
// Encapsulated Table execution with explicit backticks
|
|
75
|
+
const sql_request = `UPDATE \`${this._tableName}\` SET ${setClause} WHERE ${whereClause}`;
|
|
76
|
+
|
|
77
|
+
// ... execute transaction safely
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Release v2.0.7 — COUNT Polymorphism, Aggregation Alias Security & Test Coverage Overhaul
|
|
2
|
+
|
|
3
|
+
This major release upgrades the query engine by introducing full polymorphic support for the `COUNT` aggregation function (supporting standard, multi-column distinct, and conditional `CASE WHEN` operations). It also fixes a key-aliasing limitation during raw aggregations and strengthens query-building validation boundaries through an extensive unit testing overhaul.
|
|
4
|
+
|
|
5
|
+
## COUNT Polymorphism (Why this evolution matters)
|
|
6
|
+
|
|
7
|
+
Previously, the query engine only accepted a raw string parameter for `COUNT` instructions. The engine now dynamically evaluates three distinct structures (String, Array, Object) passed directly from your selection attributes:
|
|
8
|
+
|
|
9
|
+
- **Multi-Column Distinct Counting**: Passing an array automatically compiles a unified multi-column unique combination statement, removing the need for manual raw SQL workarounds.
|
|
10
|
+
|
|
11
|
+
- **Conditional Counting (`CASE WHEN`)**: Passing a key/value object cleanly compiles a conditional statement. This allows you to isolate state and status indicators (KPIs) in a single database round-trip without twisting your global query filters.
|
|
12
|
+
|
|
13
|
+
As a result, complex nullity checks or status distributions are generated natively:
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
-- Automated nullity indicator compilation example:
|
|
17
|
+
COUNT(CASE WHEN `deletedAt` = NULL THEN 1 END)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Test Coverage Expansion
|
|
21
|
+
|
|
22
|
+
To guarantee the stability of these new mechanics and prevent structural regressions, the global unit testing footprint has been exhaustively expanded:
|
|
23
|
+
|
|
24
|
+
- **Multi-Count Validation**: Added dedicated test pipelines ensuring the syntax of `COUNT(DISTINCT column1, column2)` is perfectly isolated from standard counters.
|
|
25
|
+
|
|
26
|
+
- **Temporal Nullity Filtering**: Implemented test scenarios covering both `{ deletedAt: null }` and `{ deletedAt: "NOT NULL" }` parameters to verify string state evaluations.
|
|
27
|
+
|
|
28
|
+
- **Unsupported Input Interception**: Added strict `throw new Error` type boundaries to guarantee that injecting non-compliant values (like booleans or numbers) results in a clean, controlled application crash.
|
|
29
|
+
|
|
30
|
+
- **Zero-Leak Key Assurances**: Added strict payload assertions ensuring no raw unmapped backticked SQL expressions can escape into the final Node.js JSON output object.
|
|
31
|
+
|
|
32
|
+
## What Was Improved
|
|
33
|
+
|
|
34
|
+
- **Automatic Aggregation Fallback Alias**: Enhanced the `buildField` logic inside `buildQuery.js`. If an explicit `as` descriptor is omitted during a column sum (sum), the engine automatically fallbacks to using the targeted column name as the extraction property, blocking raw SQL formula leaks into the final row keys.
|
|
35
|
+
|
|
36
|
+
- **Orchestrated Order Sorting**: Hardened the `options.orderBy` evaluator array loop to cleanly isolate and support both raw column identifier strings and structured sorting configuration objects `{ field, direction }`.
|
|
37
|
+
|
|
38
|
+
Documentation Cleanup: Fully aligned the core developer usage guide and documentation files within `README.md` to reflect the updated parameter boundaries and capabilities.
|
package/socket.yml
ADDED
package/src/models/Model.js
CHANGED
|
@@ -4,8 +4,7 @@ const { getConnexion } = require("../db/connexion");
|
|
|
4
4
|
const generateCondition = require("../utils/generateCondition");
|
|
5
5
|
const formatObject = require("../utils/formatObject");
|
|
6
6
|
const { ModelInstance } = require("./ModelInstance");
|
|
7
|
-
const { buildSelect, buildQueryParts } = require("../utils/buildQuery");
|
|
8
|
-
const util = require("util");
|
|
7
|
+
const { buildSelect, buildQueryParts } = require("../utils/buildQuery/buildQuery");
|
|
9
8
|
const { getSafe, setSafe } = require("../utils/security/safe");
|
|
10
9
|
const { escapeIdentifier, escapeIdentifierList } = require("../utils/sql");
|
|
11
10
|
|
|
@@ -14,10 +13,8 @@ function getFieldType(field) {
|
|
|
14
13
|
if (field.type && field.type.name !== undefined) return field.type.name;
|
|
15
14
|
else if (field.type !== undefined) return field.type;
|
|
16
15
|
return undefined;
|
|
17
|
-
} else {
|
|
18
|
-
if (field && field.name !== undefined) return field.name;
|
|
19
|
-
else return field;
|
|
20
16
|
}
|
|
17
|
+
if (field && field.name !== undefined) return field.name;
|
|
21
18
|
}
|
|
22
19
|
|
|
23
20
|
function isDateLikeType(fieldType) {
|
|
@@ -99,7 +96,7 @@ function getColumnDefinition(fieldName, field) {
|
|
|
99
96
|
if (defaultDefinition !== null) colDef += ` ${defaultDefinition}`;
|
|
100
97
|
if (field.unique) colDef += ' UNIQUE';
|
|
101
98
|
if (field.auto_increment) colDef += ' AUTO_INCREMENT';
|
|
102
|
-
if (field.primary_key) colDef += ' PRIMARY KEY'
|
|
99
|
+
if (field.primary_key) colDef += ' PRIMARY KEY'
|
|
103
100
|
if (typeof field.customize === 'string' && field.customize.length != 0) colDef += ` ${field.customize}`;
|
|
104
101
|
return `${escapeIdentifier(fieldName)} ${colDef}`;
|
|
105
102
|
}
|
|
@@ -213,18 +210,6 @@ class Model {
|
|
|
213
210
|
return `CREATE TABLE IF NOT EXISTS ${escapeIdentifier(this.name)} (${columns.join(', ')}${foreignKey.length > 0 ? ", " + foreignKey.join(', ') : ""}) ENGINE=InnoDB`;
|
|
214
211
|
}
|
|
215
212
|
|
|
216
|
-
getRecordData() {
|
|
217
|
-
return Array.isArray(this.data) ? this.data[0] ?? this.data : this.data;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
toJSON() {
|
|
221
|
-
return this.getRecordData()?.data;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
[util.inspect.custom]() {
|
|
225
|
-
return this.getRecordData();
|
|
226
|
-
}
|
|
227
|
-
|
|
228
213
|
/**
|
|
229
214
|
* Saves data to the database table.
|
|
230
215
|
* @param {Object} data The data to insert into the table.
|
|
@@ -246,23 +231,40 @@ class Model {
|
|
|
246
231
|
|
|
247
232
|
/**
|
|
248
233
|
* @typedef {Object} SelectAggregation
|
|
249
|
-
* @property {string} [
|
|
250
|
-
* @property {string} [
|
|
251
|
-
* @property {string
|
|
252
|
-
* @property {string
|
|
234
|
+
* @property {string} [col] - Le nom brut de la colonne à récupérer sans agrégation.
|
|
235
|
+
* @property {string} [sum] - Le nom de la colonne à additionner (ex: "total_runs").
|
|
236
|
+
* @property {string} [distinct] - Applique le mot-clé DISTINCT sur la colonne spécifiée.
|
|
237
|
+
* @property {string | string[] | Object} [count] - Compte les lignes selon le format fourni :
|
|
238
|
+
* - `string`: Compte toutes les valeurs non-nulles de cette colonne (ex: "id").
|
|
239
|
+
* - `string[]`: Compte les combinaisons uniques de plusieurs colonnes (COUNT DISTINCT col1, col2).
|
|
240
|
+
* - `Object`: Comptage conditionnel (CASE WHEN clé = valeur THEN 1 END). Ex: { deletedAt: null }.
|
|
241
|
+
* @property {string[]} [dateFormat] - Tableau au format [colonne, format_mysql] (ex: ["date_day", "%Y-%m-%d"]).
|
|
242
|
+
* @property {string} [as] - Alias d'extraction SQL pour le champ (ex: "total_runs" ou "period").
|
|
243
|
+
*/
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* @typedef {Object} OrderByOption
|
|
247
|
+
* @property {string} field - Le nom de la colonne sur laquelle appliquer le tri.
|
|
248
|
+
* @property {'ASC'|'DESC'} [direction] - La direction du tri (Par défaut : 'ASC').
|
|
249
|
+
*/
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* @typedef {Object} JoinOption
|
|
253
|
+
* @property {string} table - Le nom de la table cible avec laquelle effectuer la jointure.
|
|
254
|
+
* @property {string} on - La condition de correspondance de la jointure (ex: "ProjectPipelines.project_id = Projects.id").
|
|
255
|
+
* @property {string} [alias] - Alias SQL optionnel à donner à la table jointe.
|
|
256
|
+
* @property {'INNER'|'LEFT'|'RIGHT'} [type] - Le type de jointure SQL à appliquer (Par défaut : 'INNER').
|
|
253
257
|
*/
|
|
254
258
|
|
|
255
259
|
/**
|
|
256
|
-
*
|
|
257
|
-
* @param {Object} [options] -
|
|
258
|
-
* @param {Array<string|SelectAggregation>} [options.select] -
|
|
259
|
-
* @param {Object} [options.where] -
|
|
260
|
-
* @param {
|
|
261
|
-
* @param {
|
|
262
|
-
* @param {
|
|
263
|
-
* @param {
|
|
264
|
-
* @param {String} [options.join.on] - Join condition.
|
|
265
|
-
* @param {String} [options.join.alias] - Alias for the joined table.
|
|
260
|
+
* Récupère plusieurs entrées de la table correspondante.
|
|
261
|
+
* @param {Object} [options] - Options de configuration de la requête SQL.
|
|
262
|
+
* @param {Array<string|SelectAggregation>} [options.select] - Liste des champs, transformations ou agrégations à retourner.
|
|
263
|
+
* @param {Object|string} [options.where] - Filtres structurés (objet clé/valeur) ou clause WHERE brute sous forme de chaîne.
|
|
264
|
+
* @param {string[]} [options.groupBy] - Tableau de colonnes ou d'expressions pour grouper les résultats (ex: ["period"]).
|
|
265
|
+
* @param {Array<string|OrderByOption>} [options.orderBy] - Règles de tri des résultats.
|
|
266
|
+
* @param {number} [options.limit] - Limite maximale du nombre de lignes à retourner.
|
|
267
|
+
* @param {JoinOption | JoinOption[]} [options.join] - Option(s) de jointure avec d'autres tables de la base de données.
|
|
266
268
|
* @returns {Promise<Array<ModelInstance>>}
|
|
267
269
|
*/
|
|
268
270
|
async find(options = {}) {
|
|
@@ -346,7 +348,7 @@ class Model {
|
|
|
346
348
|
return resolve(1);
|
|
347
349
|
}).catch((err) => {
|
|
348
350
|
error(`Error executing query delete: ${err}`);
|
|
349
|
-
|
|
351
|
+
reject(err)
|
|
350
352
|
});
|
|
351
353
|
});
|
|
352
354
|
}
|