@mlagie/sql-connector 1.4.6 → 1.4.8

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 CHANGED
@@ -1,31 +1,18 @@
1
- # sql-connector
2
- ![GitHub package.json version](https://img.shields.io/github/package-json/v/lagie-marin/sql-connector?color=#008000) ![NPM Downloads](https://img.shields.io/npm/d18m/%40mlagie%2Fsql-connector?color=#008000) ![NPM Downloads](https://img.shields.io/npm/dw/%40mlagie%2Fsql-connector?color=#008000) ![GitHub followers](https://img.shields.io/github/followers/lagie-marin?style=plastic&color=color%3D%23008000) ![GitHub repo size](https://img.shields.io/github/repo-size/lagie-marin/sql-connector?color=%green)
3
- ![GitHub last commit](https://img.shields.io/github/last-commit/lagie-marin/sql-connector)
1
+ # sql-connector documentation
4
2
 
5
- # Documentation du module `sql-connector`
3
+ [Français](./docs/fr/README.md) | English
6
4
 
7
- 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
+ sql-connector helps manage MySQL connections, define table schemas, sync tables automatically, and work with database models through a small API.
8
6
 
9
- ## Importation du module
7
+ ## Import
10
8
 
11
9
  ```javascript
12
- const { Schema, connect, logout, Model, client, sqlTypeMap } = require('sql-connector');
10
+ const { Schema, connect, logout, Model, ModelInstance, client, sqlTypeMap } = require('sql-connector');
13
11
  ```
14
12
 
15
- ### Fonctions
13
+ ## Database connection
16
14
 
17
- `connect(config)` Établit une connexion à la base de données.
18
-
19
- * Paramètres:
20
- * `config` (Object) : La configuration de la connexion à la base de données.
21
- * `host` (string) : L'hôte de la base de données.
22
- * `port` (number) : Le port de la base de données.
23
- * `user` (string) : Le nom d'utilisateur pour la connexion.
24
- * `password` (string) : Le mot de passe pour la connexion.
25
- * `database` (string) : Le nom de la base de données.
26
- * `ect` pour en savoir plus vous pouvez vous rendre sur https://github.com/mysqljs/mysql dans la section `Connection options`
27
-
28
- * ### Exemple:
15
+ `connect(config)` opens a MySQL connection using a configuration object compatible with mysql2.
29
16
 
30
17
  ```javascript
31
18
  const config = {
@@ -35,253 +22,137 @@ const config = {
35
22
  password: 'password',
36
23
  database: 'mydatabase'
37
24
  };
25
+
38
26
  await connect(config);
39
27
  ```
40
28
 
41
- `logout()` Ferme la connexion à la base de données.
42
-
43
- * ### Exemple:
29
+ `logout()` closes the active connection.
44
30
 
45
31
  ```javascript
46
32
  await logout();
47
33
  ```
48
34
 
49
- ### Interface `SchemaField`
50
-
51
- Décrit les propriétés d'un champ dans le schéma d'une table SQL. Chaque clé du dictionnaire passé au constructeur de `Schema` doit respecter cette interface.
35
+ ## Schema
52
36
 
53
- | Propriété | Type | Description |
54
- |------------------|--------------------------------------|-----------------------------------------------------------------------------|
55
- | type | `SqlType` ou `{ name: SqlType }` | Type du champ (String, Number, Boolean, etc.) |
56
- | length | `number` | Longueur maximale (pour VARCHAR ou INT) |
57
- | required | `boolean` | Si le champ est obligatoire (NOT NULL) |
58
- | default | `any` | Valeur par défaut |
59
- | unique | `boolean` | Si le champ doit être unique |
60
- | auto_increment | `boolean` | Si le champ est auto-incrémenté |
61
- | foreignKey | `string` | Clé étrangère (ex: "otherTable(column)") |
62
- | enum | `string[]` | Liste de valeurs pour un champ ENUM |
63
- | primary_key | `boolean` | Si le champ est une clé primaire |
64
- | customize | `string` | Ajout d'options SQL personnalisées |
37
+ `Schema` describes the structure of a table. Each field can use the following properties.
65
38
 
66
- #### Exemple d'utilisation
39
+ | Property | Type | Description |
40
+ |---|---|---|
41
+ | type | `SqlType` or `{ name: SqlType }` | SQL type for the field |
42
+ | length | `number` | Maximum length |
43
+ | required | `boolean` | Not null constraint |
44
+ | default | `any` | Default value |
45
+ | unique | `boolean` | Unique constraint |
46
+ | auto_increment | `boolean` | Auto increment |
47
+ | foreignKey | `string` | Foreign key reference |
48
+ | enum | `string[]` | Allowed values |
49
+ | primary_key | `boolean` | Primary key flag |
50
+ | customize | `string` | Extra SQL options |
67
51
 
68
52
  ```javascript
69
53
  const userSchema = new Schema({
70
- status: {
71
- type: String,
72
- enum: ["active", "inactive", "pending"], // champ ENUM
73
- required: true,
74
- default: "pending"
75
- },
76
- id: {
77
- type: Number,
78
- auto_increment: true,
79
- primary_key: true
80
- },
81
- email: {
82
- type: String,
83
- length: 255,
84
- unique: true,
85
- required: true
86
- }
54
+ id: {
55
+ type: Number,
56
+ auto_increment: true,
57
+ primary_key: true
58
+ },
59
+ email: {
60
+ type: String,
61
+ length: 255,
62
+ unique: true,
63
+ required: true
64
+ },
65
+ status: {
66
+ type: String,
67
+ enum: ['active', 'inactive', 'pending'],
68
+ default: 'pending'
69
+ }
87
70
  });
88
71
  ```
89
72
 
90
- ### Classes `Schema`
73
+ ## Table synchronization
91
74
 
92
- Représente un schéma de base de données.
93
- * Constructeur:
94
- * Schema(schemaDict) : Crée une instance de Schema.
95
- * schemaDict (Object) : Un dictionnaire définissant le schéma.
75
+ `Model.syncAllTables()` compares JS schemas with the database and applies only meaningful differences.
96
76
 
97
- * Exemple
77
+ - New columns are added automatically.
78
+ - Removed columns are only dropped with `dangerousSync: true`.
79
+ - Column renames are supported through `oldName`.
80
+ - Orphan tables are backed up to a `backup_*.sql` file before deletion.
98
81
 
99
82
  ```javascript
100
- const transferSchema = new Schema({
101
- token: {
102
- type: String,
103
- length: 50
104
- },
105
- mdp: {
106
- type: String,
107
- length: 15
108
- }
109
- });
83
+ await Model.syncAllTables();
84
+ await Model.syncAllTables({ dangerousSync: true });
110
85
  ```
111
86
 
112
- ## Synchronisation automatique du schéma et gestion des migrations
113
-
114
- > **⚠️ Important : Synchronisation du schéma et sécurité**
115
- >
116
- > Toutes les opérations de synchronisation avancée du schéma (modification de type, contraintes, renommage, suppression de colonnes, etc.) **ne sont effectuées que si vous activez explicitement l’option `{ dangerousSync: true }`** lors de l’appel à `Model.syncAllTables`.
117
- >
118
- > Par défaut (`dangerousSync: false`), aucune modification de structure n’est appliquée sur les tables existantes pour garantir la sécurité de vos données en production.
119
- > Utilisez `dangerousSync` uniquement dans un environnement de développement ou lors de migrations contrôlées.
120
-
121
- ### Ajout, suppression et renommage de colonnes
122
-
123
- - **Ajout automatique de colonnes**
124
- Lorsque vous ajoutez un champ dans le schéma JS, la colonne correspondante est automatiquement ajoutée dans la base de données lors de la synchronisation avec :
125
- ```js
126
- await Model.syncAllTables();
127
- ```
128
-
129
- - **Suppression automatique de colonnes**
130
- Si vous retirez un champ du schéma JS, la colonne reste dans la base par défaut.
131
- Pour supprimer automatiquement les colonnes disparues, utilisez :
132
- ```js
133
- await Model.syncAllTables({ dangerousSync: true });
134
- ```
135
- ⚠️ Attention, cela supprime les données de ces colonnes.
136
-
137
- - **Renommage de colonne sans perte de données**
138
- Pour renommer une colonne, ajoutez la propriété `oldName` dans le schéma :
139
- ```js
140
- const userSchema = new Schema({
141
- role: { type: String, oldName: "rang" }
142
- });
143
- ```
144
- Lors de la synchronisation, la colonne SQL sera renommée sans perte de données.
145
- Un avertissement s’affichera pour vous rappeler de retirer `oldName` du schéma après migration.
146
-
147
- ### Suppression et sauvegarde des tables orphelines
148
-
149
- - Si une table SQL n’a plus de schéma JS associé, elle est supprimée automatiquement lors de la synchronisation.
150
- - **Avant suppression**, un fichier de backup SQL (INSERTs) est généré dans le dossier courant (ex : `backup_MaTable_1690000000000.sql`).
87
+ Important: do not set both `primary_key: true` and `unique: true` on the same field. A primary key is already unique and not null.
151
88
 
152
- ### Restauration et gestion des backups
89
+ ## Models
153
90
 
154
- - Si une table supprimée réapparaît dans le schéma, le module détecte la présence d’un backup et propose :
155
- 1. **De restaurer les données** (exécution du fichier SQL).
156
- 2. **De supprimer le backup** après restauration ou non.
157
- 3. Si vous refusez la suppression, le backup est renommé en `.ignored` et ne sera plus proposé.
91
+ `Model` represents a SQL table.
158
92
 
159
- ### Synchronisation intelligente des colonnes
93
+ Main methods:
160
94
 
161
- Lors de la synchronisation (`Model.syncAllTables()`), le module compare chaque colonne existante avec la définition du schéma JS :
95
+ - `save(data)` inserts a row
96
+ - `findOne(filter, fields)` fetches a single row
97
+ - `find(filter, fields)` fetches multiple rows
98
+ - `findAll(options)` supports advanced queries
99
+ - `count(filter)` counts rows
100
+ - `customRequest(custom)` runs a custom SQL query
101
+ - `delete(filter)` deletes a row
102
+ - `dropTable()` drops the table
103
+ - `generate_uuid()` generates a unique UUID
104
+ - `Model.createAllTables()` creates tables in dependency order
162
105
 
163
- - **Type** : Si le type SQL attendu diffère de celui en base, la colonne est modifiée.
164
- - **Nullabilité** : Si la contrainte `NOT NULL` ou `NULL` diffère, la colonne est modifiée.
165
- > ⚠️ Une colonne `primary_key` est toujours considérée comme `NOT NULL` même si `required` n'est pas précisé.
166
- - **Valeur par défaut** : Si la valeur par défaut diffère, la colonne est modifiée.
167
- - **Unique** : Si la contrainte `UNIQUE` diffère, la colonne est modifiée.
168
- - **Primary key** : Si la contrainte `PRIMARY KEY` diffère, la colonne est modifiée.
169
-
170
- Seules les différences réelles entraînent une modification SQL, ce qui évite les migrations inutiles.
171
-
172
- > **Note :**
173
- > Il est interdit de déclarer une colonne à la fois `primary_key: true` et `unique: true` dans le schéma JS.
174
- > **Explication :** En SQL, une clé primaire (`PRIMARY KEY`) est déjà unique par définition et impose la contrainte `UNIQUE` et `NOT NULL` sur la colonne. Ajouter explicitement `unique: true` en plus de `primary_key: true` est redondant et provoque une erreur SQL ("Multiple primary key defined").
175
- >
176
- > **En résumé :**
177
- > - Utilisez seulement `primary_key: true` pour une colonne qui doit être la clé primaire.
178
- > - Utilisez `unique: true` pour une colonne qui doit être unique mais n'est pas la clé primaire.
106
+ ```javascript
107
+ const userModel = new Model('users', userSchema);
179
108
 
180
- ---
109
+ await Model.createAllTables();
110
+ await userModel.save({ email: 'user@example.com', status: 'active' });
181
111
 
112
+ const user = await userModel.findOne({ email: 'user@example.com' });
113
+ await userModel.delete({ email: 'user@example.com' });
114
+ ```
182
115
 
183
- #### Exemple d’utilisation
116
+ ## Model instances
184
117
 
185
- ```js
186
- // Synchronisation simple (ajout/renommage de colonnes, suppression de tables orphelines avec backup)
187
- await Model.syncAllTables();
118
+ `ModelInstance` represents a row already loaded from the database.
188
119
 
189
- // Synchronisation avec suppression automatique des colonnes disparues
190
- await Model.syncAllTables({ dangerousSync: true });
191
- ```
120
+ - `updateOne(model)` updates the row
121
+ - `delete(model)` deletes the row using a filter
122
+ - `deleteOne()` deletes the instance row
123
+ - `customRequest(custom)` runs a custom query
192
124
 
193
- ---
194
-
195
- ## Class Model
196
- Représente un modèle de base de données.
197
- * ### Constructeur :
198
- * `Model(name, schema)` : Crée une instance de `Model`.
199
- * `name (string)` : Le nom de la table de base de données.
200
- * `schema (Schema)` : Le schéma de la table de base de données.
201
- * ### Méthodes :
202
- * `generateCreateTableStatement(schema)` : Génère une requête SQL pour créer une table.
203
- * `save(data)` : Sauvegarde des données dans la table.
204
- * `findOne(filter, fields)` : Trouve une entrée unique dans la table.
205
- * `find(filter, fields)` : Trouve des entrées dans la table.
206
- * `customRequest(custom)` : Exécute une requête SQL personnalisée.
207
- * `delete(filter)` : Supprime une entrée de la table.
208
- * `dropTable()` : Supprime la table si elle existe.
209
- * `generate_uuid()` : Génère un UUID unique pour le modèle.
210
- * `createAllTables()` : **Crée toutes les tables dans le bon ordre selon les dépendances de clés étrangères.** (statique)
211
- * ### Exemple
212
125
  ```javascript
213
- const userModel = new Model('users', transferSchema);
214
- // Après avoir instancié tous les modèles :
215
- await Model.createAllTables(); // Crée toutes les tables dans le bon ordre
216
-
217
- // Sauvegarder des données
218
- await userModel.save({ token: 'abc123', mdp: 'password' });
126
+ const userInstance = new ModelInstance('users', { email: 'user@example.com' });
219
127
 
220
- // Trouver une entrée
221
- const user = await userModel.findOne({ token: 'abc123' });
222
-
223
- // Supprimer une entrée
224
- await userModel.delete({ token: 'abc123' });
128
+ await userInstance.updateOne({ status: 'inactive' });
129
+ await userInstance.deleteOne();
225
130
  ```
226
- ## Class ModelInstance
227
- Représente une instance d'un modèle de base de données.
228
- * ### Constructeur:
229
- * `ModelInstance(name, data)` : Crée une instance de `ModelInstance`.
230
- * `name (string)` : Le nom de la table de base de données.
231
- * `data (Object)` : Les données de l'instance.
232
- * ### Méthodes :
233
- * `updateOne(model)` : Met à jour une entrée unique dans la table.
234
- * `delete(model)` : Supprime une entrée unique dans la table.
235
- * `customRequest(custom)` : Exécute une requête SQL personnalisée.
236
- * ### Exemple
237
- ```javascript
238
- const userInstance = new ModelInstance('users', { token: 'abc123', mdp: 'password' });
239
131
 
240
- // Mettre à jour des données
241
- await userInstance.updateOne({ mdp: 'newpassword' });
132
+ ## SQL types
133
+
134
+ `sqlTypeMap` exposes the common SQL types.
242
135
 
243
- // Supprimer des données
244
- await userInstance.delete({ token: 'abc123' });
245
- ```
246
- # Types SQL
247
- Le module fournit une map des types SQL courants via `sqlTypeMap`.
248
- ## Type
249
- * String
250
- * Number
251
- * Boolean
252
- * Object
253
- * Array
254
- * Now
255
- * Float
256
- * Text
257
- * DateTime
258
- * Timestamp
259
- * ### Exemple
260
136
  ```javascript
261
137
  console.log(sqlTypeMap.String); // "VARCHAR"
262
138
  ```
263
- ## Conclusion
264
- Le module `sql-connector` fournit une interface simple et efficace pour interagir avec une base de données MySQL, permettant de définir des schémas, de gérer des connexions, et de manipuler des données de manière intuitive.
265
139
 
266
- # Client
267
- Le module client est un objet utilisé pour stocker des fonctions. Il sert de conteneur centralisé pour diverses fonctions qui peuvent être utilisées dans différentes parties de l'application.
140
+ ## Client
141
+
142
+ `client` is a shared object meant to host reusable application functions.
268
143
 
269
- ### Utilisation
270
- Pour ajouter une fonction à l'objet client, vous pouvez simplement définir une nouvelle propriété sur l'objet et lui assigner une fonction.
271
- * ### Exemple
272
144
  ```javascript
273
145
  module.exports = client => {
274
- client.checkServer() {
275
- if (server.islaunch())
276
- return 1;
277
- return 0;
278
- };
146
+ client.checkServer = () => {
147
+ if (server.isLaunch()) {
148
+ return 1;
149
+ }
150
+
151
+ return 0;
152
+ };
279
153
  };
280
154
  ```
281
- ### Avantages
282
- * `Centralisation` : Toutes les fonctions liées à des opérations spécifiques peuvent être centralisées dans un seul objet, ce qui facilite la gestion et l'organisation du code.
283
- * `Réutilisabilité` : Les fonctions stockées dans l'objet `client` peuvent être facilement réutilisées dans différentes parties de l'application.
284
- * `Modularité` : En utilisant un objet pour stocker des fonctions, il est plus facile de maintenir et de mettre à jour le code, car les fonctions peuvent être ajoutées, modifiées ou supprimées sans affecter d'autres parties de l'application.
285
155
 
286
- ## Conclusion
287
- L'objet `client` est un outil puissant pour organiser et centraliser les fonctions dans votre application. En stockant des fonctions dans cet objet, vous pouvez améliorer la modularité, la réutilisabilité et la maintenabilité de votre code.
156
+ ## Summary
157
+
158
+ sql-connector provides a small layer to connect to MySQL, describe schemas, synchronize tables, and manipulate data with typed models.
@@ -0,0 +1,158 @@
1
+ # Documentation du module sql-connector
2
+
3
+ [English](../../README.md) | Français
4
+
5
+ Le module sql-connector permet de gérer des connexions MySQL, de définir des schémas, de synchroniser automatiquement des tables et d'exposer des modèles pour manipuler les données simplement.
6
+
7
+ ## Importation
8
+
9
+ ```javascript
10
+ const { Schema, connect, logout, Model, ModelInstance, client, sqlTypeMap } = require('sql-connector');
11
+ ```
12
+
13
+ ## Connexion à la base
14
+
15
+ `connect(config)` ouvre une connexion MySQL à partir d'un objet de configuration compatible avec mysql2.
16
+
17
+ ```javascript
18
+ const config = {
19
+ host: 'localhost',
20
+ port: 3306,
21
+ user: 'root',
22
+ password: 'password',
23
+ database: 'mydatabase'
24
+ };
25
+
26
+ await connect(config);
27
+ ```
28
+
29
+ `logout()` ferme proprement la connexion.
30
+
31
+ ```javascript
32
+ await logout();
33
+ ```
34
+
35
+ ## Schéma
36
+
37
+ `Schema` décrit la structure d'une table. Chaque champ peut utiliser les propriétés suivantes.
38
+
39
+ | Propriété | Type | Description |
40
+ |---|---|---|
41
+ | type | `SqlType` ou `{ name: SqlType }` | Type SQL du champ |
42
+ | length | `number` | Longueur maximale |
43
+ | required | `boolean` | Champ obligatoire |
44
+ | default | `any` | Valeur par défaut |
45
+ | unique | `boolean` | Valeur unique |
46
+ | auto_increment | `boolean` | Auto-incrément |
47
+ | foreignKey | `string` | Référence de clé étrangère |
48
+ | enum | `string[]` | Liste de valeurs autorisées |
49
+ | primary_key | `boolean` | Clé primaire |
50
+ | customize | `string` | Options SQL additionnelles |
51
+
52
+ ```javascript
53
+ const userSchema = new Schema({
54
+ id: {
55
+ type: Number,
56
+ auto_increment: true,
57
+ primary_key: true
58
+ },
59
+ email: {
60
+ type: String,
61
+ length: 255,
62
+ unique: true,
63
+ required: true
64
+ },
65
+ status: {
66
+ type: String,
67
+ enum: ['active', 'inactive', 'pending'],
68
+ default: 'pending'
69
+ }
70
+ });
71
+ ```
72
+
73
+ ## Synchronisation des tables
74
+
75
+ `Model.syncAllTables()` compare les schémas JS avec la base et applique uniquement les différences utiles.
76
+
77
+ - Ajout de colonne: automatique.
78
+ - Suppression de colonne: uniquement avec `dangerousSync: true`.
79
+ - Renommage de colonne: possible avec `oldName`.
80
+ - Tables orphelines: sauvegarde avant suppression dans un fichier `backup_*.sql`.
81
+
82
+ ```javascript
83
+ await Model.syncAllTables();
84
+ await Model.syncAllTables({ dangerousSync: true });
85
+ ```
86
+
87
+ Point important: ne combinez pas `primary_key: true` et `unique: true` sur le même champ. Une clé primaire est déjà unique et non nulle.
88
+
89
+ ## Modèles
90
+
91
+ `Model` représente une table SQL.
92
+
93
+ Méthodes principales:
94
+
95
+ - `save(data)` pour insérer une ligne
96
+ - `findOne(filter, fields)` pour récupérer une seule entrée
97
+ - `find(filter, fields)` pour récupérer plusieurs entrées
98
+ - `findAll(options)` pour les recherches avancées
99
+ - `count(filter)` pour compter les lignes
100
+ - `customRequest(custom)` pour exécuter une requête SQL personnalisée
101
+ - `delete(filter)` pour supprimer une entrée
102
+ - `dropTable()` pour supprimer la table
103
+ - `generate_uuid()` pour générer un UUID unique
104
+ - `Model.createAllTables()` pour créer toutes les tables dans le bon ordre
105
+
106
+ ```javascript
107
+ const userModel = new Model('users', userSchema);
108
+
109
+ await Model.createAllTables();
110
+ await userModel.save({ email: 'user@example.com', status: 'active' });
111
+
112
+ const user = await userModel.findOne({ email: 'user@example.com' });
113
+ await userModel.delete({ email: 'user@example.com' });
114
+ ```
115
+
116
+ ## Instances de modèle
117
+
118
+ `ModelInstance` représente une ligne déjà chargée depuis la base.
119
+
120
+ - `updateOne(model)` met à jour la ligne
121
+ - `delete(model)` supprime la ligne avec un filtre
122
+ - `deleteOne()` supprime la ligne de l'instance
123
+ - `customRequest(custom)` exécute une requête personnalisée
124
+
125
+ ```javascript
126
+ const userInstance = new ModelInstance('users', { email: 'user@example.com' });
127
+
128
+ await userInstance.updateOne({ status: 'inactive' });
129
+ await userInstance.deleteOne();
130
+ ```
131
+
132
+ ## Types SQL
133
+
134
+ `sqlTypeMap` expose les types SQL les plus courants.
135
+
136
+ ```javascript
137
+ console.log(sqlTypeMap.String); // "VARCHAR"
138
+ ```
139
+
140
+ ## Client
141
+
142
+ `client` est un objet partagé pensé pour centraliser des fonctions applicatives réutilisables.
143
+
144
+ ```javascript
145
+ module.exports = client => {
146
+ client.checkServer = () => {
147
+ if (server.isLaunch()) {
148
+ return 1;
149
+ }
150
+
151
+ return 0;
152
+ };
153
+ };
154
+ ```
155
+
156
+ ## Résumé
157
+
158
+ sql-connector fournit une couche simple pour connecter une base MySQL, décrire des schémas, synchroniser les tables et manipuler les données avec des modèles typés.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mlagie/sql-connector",
3
- "version": "1.4.6",
3
+ "version": "1.4.8",
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": {
@@ -35,8 +35,8 @@
35
35
  "homepage": "https://github.com/lagie-marin/sql-connector#readme",
36
36
  "private": false,
37
37
  "dependencies": {
38
- "@mlagie/logger": "1.0.1",
39
- "glob": "^11.0.3",
40
- "mysql2": "3.15.1"
38
+ "@mlagie/logger": "1.0.2",
39
+ "glob": "^13.0.6",
40
+ "mysql2": "3.22.4"
41
41
  }
42
42
  }
@@ -0,0 +1,43 @@
1
+ # **Release v1.4.8 — sql-connector**
2
+
3
+ ## **Présentation**
4
+ Cette version poursuit la refonte de `sql-connector` avec une documentation plus lisible, une séparation claire entre les langues, et plusieurs améliorations de fiabilité sur les modèles et la génération SQL.
5
+
6
+ ## **Nouveautés**
7
+
8
+ ### Documentation bilingue
9
+ - La documentation a été restructurée pour séparer plus clairement le contenu en français et en anglais.
10
+ - Le README racine sert désormais de point d'entrée rapide vers la documentation.
11
+ - Le dossier `docs/` est utilisé pour regrouper les pages de documentation par langue.
12
+
13
+ ### Synchronisation et schémas
14
+ - `Model.syncAllTables()` continue de comparer le schéma JS avec la base et applique les différences utiles de manière plus sûre.
15
+ - La longueur par défaut est désormais gérée correctement pour les types `VARCHAR` et `INT` quand `length` n'est pas défini.
16
+ - Une erreur explicite est levée si un champ est déclaré à la fois `primary_key` et `unique`.
17
+
18
+ ### Modèles et requêtes
19
+ - `updateOne()` ignore automatiquement les champs `undefined`.
20
+ - Les mises à jour utilisent une condition `WHERE` plus robuste, en privilégiant les clés primaires quand elles sont disponibles.
21
+ - La génération des conditions SQL a été améliorée pour mieux normaliser les chaînes, les dates et les valeurs JSON.
22
+
23
+ ### Dépendances et maintenance
24
+ - `glob` a été ajouté pour améliorer la détection des fichiers de backup SQL.
25
+ - Les logs de synchronisation et d'exécution ont été renforcés pour faciliter le débogage.
26
+
27
+ ## **Migration depuis v1.4.5**
28
+
29
+ ### Points principaux
30
+ 1. La documentation a été réorganisée autour d'un accès plus simple par langue.
31
+ 2. Les opérations de mise à jour de modèles sont maintenant plus fiables.
32
+ 3. Les règles de génération SQL sont plus strictes et plus prévisibles.
33
+
34
+ ### Recommandation
35
+ - Vérifiez vos schémas si vous utilisiez des champs sans `length`, des mises à jour avec des valeurs `undefined`, ou des colonnes déclarées à la fois `primary_key` et `unique`.
36
+
37
+ ## **Documentation**
38
+ Pour plus de détails, consultez la documentation du projet dans `README.md` et le dossier `docs/`.
39
+
40
+ ## **Liens utiles**
41
+ - [GitHub Repository](https://github.com/lagie-marin/sql-connector)
42
+ - [npm Package](https://www.npmjs.com/package/@mlagie/sql-connector)
43
+ - [Issues](https://github.com/lagie-marin/sql-connector/issues)
@@ -1,4 +1,4 @@
1
- const { logs, error, sql } = require("@mlagie/logger");
1
+ const { logs, error } = require("@mlagie/logger");
2
2
  const { sqlTypeMap } = require("../utils/sqlTypeMap");
3
3
  const { getConnexion } = require("../db/connexion");
4
4
  const generateCondition = require("../utils/generateCondition");
@@ -505,7 +505,7 @@ class Model {
505
505
  async save(data) {
506
506
  const keys = Object.keys(data);
507
507
  const sql_request = `INSERT INTO ${this.name} (${keys.join(', ')}) VALUES (${generateValueSQL(Object.values(data))})`;
508
- sql(this.name, sql_request);
508
+
509
509
  try {
510
510
  const result = await getConnexion().promise().query(sql_request);
511
511
  return result;
@@ -604,7 +604,7 @@ class Model {
604
604
  getConnexion().promise().query(sql_request).then((rows) => {
605
605
  if (rows.length == 0) return resolve(0);
606
606
 
607
- resolve(new ModelInstance(this.name, Object.values(rows[0]), this.schema));
607
+ resolve(new ModelInstance(this.name, rows[0], this.schema));
608
608
  }).catch((err) => {
609
609
  error(`Error executing query: ${err}`);
610
610
  return;
@@ -632,7 +632,7 @@ class Model {
632
632
  await getConnexion().promise().query(custom).then((rows) => {
633
633
  if (rows.length == 0) return resolve(0);
634
634
 
635
- resolve(new ModelInstance(this.name, Object.values(rows[0]), this.schema));
635
+ resolve(new ModelInstance(this.name, rows[0], this.schema));
636
636
  }).catch((err) => {
637
637
  error(`Error executing query: ${err}`);
638
638
  return;
@@ -1,7 +1,8 @@
1
- const { error } = require("@mlagie/logger");
1
+ const { error, logs } = require("@mlagie/logger");
2
2
  const { getConnexion } = require("../db/connexion");
3
3
  const formatObject = require("../utils/formatObject");
4
4
  const generateCondition = require("../utils/generateCondition");
5
+ const util = require("util");
5
6
 
6
7
  /**
7
8
  * Represents an instance of a database model.
@@ -15,23 +16,38 @@ class ModelInstance {
15
16
  * @param {Object|null} [schema=null] The schema for the instance, if available.
16
17
  */
17
18
  constructor(name, data, schema = null) {
18
- /**
19
- * The name of the database table.
20
- * @type {string}
21
- */
22
- this.name = name;
23
-
24
- /**
25
- * The instance data.
26
- * @type {Object}
27
- */
28
- this.data = data;
29
-
30
- /**
31
- * The schema for the instance.
32
- * @type {Object|null}
33
- */
34
- this.schema = schema;
19
+ Object.defineProperties(this, {
20
+ name: {
21
+ value: name,
22
+ writable: true,
23
+ configurable: true,
24
+ enumerable: false
25
+ },
26
+ data: {
27
+ value: data,
28
+ writable: true,
29
+ configurable: true,
30
+ enumerable: true
31
+ },
32
+ schema: {
33
+ value: schema,
34
+ writable: true,
35
+ configurable: true,
36
+ enumerable: false
37
+ }
38
+ });
39
+ }
40
+
41
+ getRecordData() {
42
+ return Array.isArray(this.data) ? this.data[0] ?? this.data : this.data;
43
+ }
44
+
45
+ toJSON() {
46
+ return this.getRecordData();
47
+ }
48
+
49
+ [util.inspect.custom]() {
50
+ return this.getRecordData();
35
51
  }
36
52
 
37
53
  /**
@@ -42,13 +58,50 @@ class ModelInstance {
42
58
  * @throws {Error} Throws an error if the update fails.
43
59
  */
44
60
  async updateOne(model) {
45
- const sql_request = `UPDATE ${this.name} SET ${generateCondition(formatObject(model), true)} WHERE ${generateCondition(formatObject(this.data[0] != undefined ? this.data[0] : this.data), false, this.schema)}`;
61
+ const setClause = generateCondition(formatObject(model), true);
62
+
63
+ // prefer primary key(s) in WHERE to avoid mismatches on nullable/text fields
64
+ let whereClause;
65
+ try {
66
+ const rec = this.getRecordData();
67
+ const schemaDict = this.schema && this.schema.schemaDict ? this.schema.schemaDict : null;
68
+ if (schemaDict) {
69
+ const pkKeys = Object.entries(schemaDict).filter(([k, v]) => v && v.primary_key === true).map(([k]) => k);
70
+ if (pkKeys.length > 0) {
71
+ const pkObj = {};
72
+ for (const k of pkKeys) {
73
+ if (rec && Object.prototype.hasOwnProperty.call(rec, k)) pkObj[k] = rec[k];
74
+ }
75
+ if (Object.keys(pkObj).length > 0) whereClause = generateCondition(formatObject(pkObj), false, this.schema);
76
+ }
77
+ }
78
+ if (!whereClause) whereClause = generateCondition(formatObject(rec), false, this.schema);
79
+ } catch (e) {
80
+ whereClause = generateCondition(formatObject(this.getRecordData()), false, this.schema);
81
+ }
82
+ const sql_request = `UPDATE ${this.name} SET ${setClause} WHERE ${whereClause}`;
83
+
84
+ // log SQL for debugging why update may not match
85
+ try { logs(`ModelInstance.updateOne SQL -> ${sql_request}`); } catch (e) {}
46
86
 
47
- await getConnexion().promise().query(sql_request).catch((err) => {
87
+ const [result] = await getConnexion().promise().query(sql_request).catch((err) => {
48
88
  error(`Error executing query: ${err}`);
49
89
  throw err;
50
90
  });
51
- return 1;
91
+
92
+ const affected = result && (result.affectedRows !== undefined ? result.affectedRows : 0);
93
+
94
+ // Update in-memory data if DB was modified
95
+ if (affected > 0) {
96
+ const record = this.getRecordData();
97
+ if (Array.isArray(this.data)) {
98
+ if (this.data[0] && typeof this.data[0] === 'object') Object.assign(this.data[0], model);
99
+ } else if (record && typeof record === 'object') {
100
+ Object.assign(this.data, model);
101
+ }
102
+ }
103
+
104
+ return affected;
52
105
  }
53
106
 
54
107
  /**
@@ -79,7 +132,7 @@ class ModelInstance {
79
132
  * @throws {Error} Throws an error if the deletion fails.
80
133
  */
81
134
  async deleteOne() {
82
- const sql_request = `DELETE FROM ${this.name} WHERE ${generateCondition(formatObject(this.data[0] != undefined ? this.data[0] : this.data))}`;
135
+ const sql_request = `DELETE FROM ${this.name} WHERE ${generateCondition(formatObject(this.getRecordData()))}`;
83
136
 
84
137
  return new Promise((resolve, reject) => {
85
138
  getConnexion().promise().query(sql_request).then((rows) => {
@@ -104,7 +157,7 @@ class ModelInstance {
104
157
  await getConnexion().promise().query(custom).then((rows) => {
105
158
  if (rows.length == 0) return resolve(0);
106
159
 
107
- resolve(new ModelInstance(this.name, Object.values(rows[0]), this.schema));
160
+ resolve(new ModelInstance(this.name, rows[0], this.schema));
108
161
  }).catch((err) => {
109
162
  error(`Error executing query: ${err}`);
110
163
  return;
@@ -2,13 +2,28 @@ module.exports = function (obj) {
2
2
  for (const key in obj) {
3
3
  if (obj.hasOwnProperty(key)) {
4
4
  const value = obj[key];
5
+ if (value instanceof Date) {
6
+ // convert Date to MySQL DATETIME (no timezone)
7
+ obj[key] = value.toISOString().slice(0, 19).replace('T', ' ');
8
+ continue;
9
+ }
10
+
5
11
  if (typeof value === "string") {
6
- obj[key] = value.replace(/"/g, '\\"');
12
+ // remove surrounding quotes if present and unescape
13
+ let v = value.trim();
14
+ // remove wrapping double or single quotes
15
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
16
+ v = v.slice(1, -1);
17
+ }
18
+ // unescape common escaped quotes
19
+ v = v.replace(/\\"/g, '"').replace(/\\'/g, "'");
20
+ obj[key] = v;
21
+ continue;
7
22
  }
23
+
8
24
  if (typeof value === "object") {
9
- obj[key] = JSON.stringify(value)
10
- .replace(/"/g, '\\"')
11
- .replace(/'/g, "\\'");
25
+ // stringify objects and escape single quotes for SQL safety
26
+ obj[key] = JSON.stringify(value).replace(/'/g, "\\'");
12
27
  }
13
28
  }
14
29
  }
@@ -29,37 +29,79 @@ module.exports = function (filter, isUpdate = false, schema = null) {
29
29
  });
30
30
  if (uniqueKeys.length > 0) {
31
31
  return uniqueKeys.map(key => {
32
- const value = filter[key];
32
+ let value = filter[key];
33
+ // normalize strings that may contain surrounding quotes or escaped quotes
34
+ if (typeof value === 'string') {
35
+ value = value.trim();
36
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
37
+ value = value.slice(1, -1);
38
+ }
39
+ value = value.replace(/\\"/g, '"').replace(/\\'/g, "'");
40
+ }
33
41
  if (Array.isArray(value)) {
34
- return `${key} IN (${value.map(v => `"${v}"`).join(", ")})`;
42
+ return `${key} IN (${value.map(v => `'${String(v).replace(/'/g, "\\'")}'`).join(", ")})`;
35
43
  }
36
44
  if (typeof value === "object" || (typeof value === "string" && value.trim().startsWith("{") && value.trim().endsWith("}"))) {
37
45
  const jsonVal = typeof value === "string" ? value : JSON.stringify(value);
38
- return `JSON_CONTAINS(${key}, '${jsonVal}')`;
46
+ return `JSON_CONTAINS(${key}, '${String(jsonVal).replace(/'/g, "\\'")}')`;
39
47
  }
40
48
  if (value === null || value === "null") return `${key} IS NULL`;
41
- return `${key} = ${typeof value === "string" ? `"${value}"` : value}`;
49
+ // if string looks like an ISO datetime, convert to MySQL DATETIME format
50
+ if (typeof value === 'string' && /T/.test(value)) {
51
+ let val = value.replace(/\.\d+Z$/,'').replace(/Z$/,'').replace('T',' ');
52
+ return `${key} = '${String(val).replace(/'/g, "\\'")}'`;
53
+ }
54
+ return `${key} = ${typeof value === "string" ? `'${String(value).replace(/'/g, "\\'")}'` : value}`;
42
55
  }).join(" AND ");
43
56
  }
44
57
  }
45
58
 
46
59
  // Comportement par défaut
47
60
  const conditions = filteredKeys.map((key, index) => {
48
- const value = filteredValues[index];
61
+ let value = filteredValues[index];
62
+ if (typeof value === 'string') {
63
+ value = value.trim();
64
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
65
+ value = value.slice(1, -1);
66
+ }
67
+ value = value.replace(/\\"/g, '"').replace(/\\'/g, "'");
68
+ }
49
69
 
50
70
  if (Array.isArray(value)) {
51
- return `${key} IN (${value.map(v => `"${v}"`).join(", ")})`;
71
+ return `${key} IN (${value.map(v => `'${String(v).replace(/'/g, "\\'")}'`).join(", ")})`;
52
72
  }
53
73
  if (typeof value === "object" || (typeof value === "string" && value.trim().startsWith("{") && value.trim().endsWith("}"))) {
54
74
  const jsonVal = typeof value === "string" ? value : JSON.stringify(value);
55
75
  if (isUpdate) {
56
- return `${key} = '${jsonVal}'`;
76
+ return `${key} = '${String(jsonVal).replace(/'/g, "\\'")}'`;
57
77
  }
58
- return `JSON_CONTAINS(${key}, '${jsonVal}')`;
78
+ return `JSON_CONTAINS(${key}, '${String(jsonVal).replace(/'/g, "\\'")}')`;
59
79
  }
60
80
 
61
81
  if ((value === null || value === "null") && isUpdate == false) return `${key} IS NULL`;
62
- return `${key} = ${typeof value === "string" ? `"${value}"` : value}`;
82
+
83
+ // handle date-like strings when schema tells us the field is temporal
84
+ const fieldDef = schema && schema.schemaDict ? schema.schemaDict[key] : null;
85
+ let fieldType = null;
86
+ if (fieldDef) {
87
+ if (fieldDef.type && fieldDef.type.name !== undefined) fieldType = fieldDef.type.name;
88
+ else if (fieldDef.type !== undefined) fieldType = fieldDef.type;
89
+ else if (fieldDef && fieldDef.name !== undefined) fieldType = fieldDef.name;
90
+ }
91
+ const normalizedFieldType = String(fieldType ?? "").toLowerCase();
92
+ const isDateLike = ["date", "datetime", "timestamp", "now"].includes(normalizedFieldType);
93
+
94
+ if (typeof value === "string") {
95
+ let val = value;
96
+ // strip surrounding quotes if any (double safety)
97
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) val = val.slice(1,-1);
98
+ // if ISO timestamp with Z, convert to MySQL DATETIME format
99
+ if (isDateLike && /T/.test(val)) {
100
+ val = val.replace(/\.\d+Z$/,'').replace(/Z$/,'').replace('T',' ');
101
+ }
102
+ return `${key} = '${String(val).replace(/'/g, "\\'")}'`;
103
+ }
104
+ return `${key} = ${value}`;
63
105
  }).join(` ${isUpdate == false ? "AND" : ","} `);
64
106
  return conditions;
65
107
  }