@mlagie/sql-connector 3.0.3 → 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 CHANGED
@@ -1,510 +1,1179 @@
1
- # sql-connector documentation
1
+ # @mlagie/sql-connector
2
2
 
3
- ![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)
4
- ![GitHub last commit](https://img.shields.io/github/last-commit/lagie-marin/sql-connector)
3
+ [![npm version](https://img.shields.io/npm/v/%40mlagie%2Fsql-connector)](https://www.npmjs.com/package/@mlagie/sql-connector)
4
+ [![npm downloads](https://img.shields.io/npm/dm/%40mlagie%2Fsql-connector)](https://www.npmjs.com/package/@mlagie/sql-connector)
5
+ [![license](https://img.shields.io/github/license/mlagie/sql-connector)](https://github.com/mlagie/sql-connector/blob/main/LICENSE)
5
6
 
6
- [Français](./docs/fr/README_FR.md) | English
7
+ A small, typed and dialect-aware SQL layer for **MySQL** and **PostgreSQL**.
7
8
 
8
- sql-connector helps manage MySQL and PostgreSQL connections, define table schemas, sync tables automatically, and work with database models through a small API.
9
+ `@mlagie/sql-connector` provides:
9
10
 
10
- ## Import
11
+ - connection management;
12
+ - declarative table schemas;
13
+ - SQL type aliases and native SQL types;
14
+ - CRUD operations through models;
15
+ - structured `WHERE` / `HAVING` conditions;
16
+ - `IN`, `BETWEEN`, `LIKE`, `ILIKE`, `NULL`, `AND`, `OR` and `NOT` conditions;
17
+ - aggregations, grouping, ordering and joins;
18
+ - foreign-key dependency ordering;
19
+ - schema synchronization;
20
+ - migration planning, approval, execution and status tracking;
21
+ - migration checksums, encrypted migration files and optional encrypted backups;
22
+ - TypeScript declarations for the public API.
11
23
 
12
- ```javascript
13
- const { Schema, connect, logout, Model, ModelInstance, client, sqlTypeMap } = require('sql-connector');
24
+ > **Supported database engines:** MySQL and PostgreSQL.
25
+ >
26
+ > SQL generated by the connector is dialect-specific where necessary. The schema API stays the same across supported engines.
27
+
28
+ **Documentation:** [English](#table-of-contents) · [Français](./docs/fr/README_FR.md) · [Detailed documentation](./docs/README.md)
29
+
30
+ ---
31
+
32
+ ## Table of contents
33
+
34
+ 1. [Installation](#installation)
35
+ 2. [Quick start](#quick-start)
36
+ 3. [Connection](#connection)
37
+ 4. [Schemas](#schemas)
38
+ - [Schema field options](#schema-field-options)
39
+ - [Types](#types)
40
+ - [Defaults and values](#defaults-and-values)
41
+ - [Constraints](#constraints)
42
+ - [Foreign keys](#foreign-keys)
43
+ 5. [Models](#models)
44
+ - [`save`](#save)
45
+ - [`find`](#find)
46
+ - [`count`](#count)
47
+ - [`delete`](#delete)
48
+ - [`dropTable`](#droptable)
49
+ - [`generate_uuid`](#generate_uuid)
50
+ - [`customRequest`](#customrequest)
51
+ 6. [Conditions](#conditions)
52
+ - [Simple values](#simple-values)
53
+ - [Comparison operators](#comparison-operators)
54
+ - [`IN` / `NOT IN`](#in--not-in)
55
+ - [`BETWEEN` / `NOT BETWEEN`](#between--not-between)
56
+ - [`NULL`](#null)
57
+ - [`AND` / `OR` / `NOT`](#and--or--not)
58
+ 7. [Select, aggregation, grouping and ordering](#select-aggregation-grouping-and-ordering)
59
+ 8. [Joins](#joins)
60
+ 9. [Schema synchronization](#schema-synchronization)
61
+ 10. [Model instances](#model-instances)
62
+ 11. [Migrations](#migrations)
63
+ - [Migration workflow](#migration-workflow)
64
+ - [Migration options](#migration-options)
65
+ - [Migration plans](#migration-plans)
66
+ - [Destructive changes](#destructive-changes)
67
+ - [Checksums](#checksums)
68
+ - [Encrypted files](#encrypted-files)
69
+ - [Backups and retention](#backups-and-retention)
70
+ - [Migration status](#migration-status)
71
+ 12. [TypeScript](#typescript)
72
+ 13. [Security and SQL safety](#security-and-sql-safety)
73
+ 14. [Dialect matrix](#dialect-matrix)
74
+ 15. [API exports](#api-exports)
75
+ 16. [Testing](#testing)
76
+ 17. [Related documentation](#related-documentation)
77
+ 18. [License](#license)
78
+
79
+ ---
80
+
81
+ ## Installation
82
+
83
+ ```bash
84
+ npm install @mlagie/sql-connector
14
85
  ```
15
86
 
16
- ## Database connection
87
+ The package includes both MySQL (`mysql2`) and PostgreSQL (`pg`) drivers.
17
88
 
18
- `connect(config, dialect)` opens a database connection. MySQL is used by default; pass `"postgres"` to use PostgreSQL.
89
+ ---
19
90
 
20
- ```javascript
21
- const config = {
91
+ ## Quick start
92
+
93
+ ```js
94
+ const {
95
+ Schema,
96
+ Model,
97
+ connect,
98
+ logout,
99
+ } = require('@mlagie/sql-connector');
100
+
101
+ await connect({
22
102
  host: 'localhost',
23
103
  port: 3306,
24
104
  user: 'root',
25
105
  password: 'password',
26
- database: 'mydatabase'
27
- };
106
+ database: 'mydatabase',
107
+ });
108
+
109
+ const userSchema = new Schema({
110
+ id: {
111
+ type: Number,
112
+ auto_increment: true,
113
+ primary_key: true,
114
+ },
115
+ email: {
116
+ type: String,
117
+ length: 255,
118
+ required: true,
119
+ unique: true,
120
+ },
121
+ status: {
122
+ type: String,
123
+ enum: ['active', 'inactive', 'pending'],
124
+ default: 'pending',
125
+ },
126
+ });
127
+
128
+ const User = new Model('users', userSchema);
129
+
130
+ await Model.syncAllTables();
131
+ await User.save({ email: 'user@example.com', status: 'active' });
132
+
133
+ const users = await User.find({
134
+ where: {
135
+ status: { IN: ['active', 'pending'] },
136
+ },
137
+ });
138
+
139
+ console.log(users);
140
+
141
+ await logout();
142
+ ```
143
+
144
+ For migrations, use the same models as the desired schema:
145
+
146
+ ```js
147
+ const { Migration } = require('@mlagie/sql-connector');
148
+
149
+ const migration = new Migration({
150
+ encryptionKey: process.env.SQL_CONNECTOR_MIGRATION_KEY,
151
+ });
152
+
153
+ const plan = await migration.plan({ name: 'sync-users' });
154
+
155
+ if (plan.hasChanges()) {
156
+ plan.approve();
157
+ await plan.execute();
158
+ }
159
+ ```
160
+
161
+ ---
162
+
163
+ ## Connection
28
164
 
29
- await connect(config);
165
+ ### MySQL
166
+
167
+ MySQL is the default dialect.
168
+
169
+ ```js
170
+ await connect({
171
+ host: 'localhost',
172
+ port: 3306,
173
+ user: 'root',
174
+ password: 'password',
175
+ database: 'mydatabase',
176
+ });
30
177
  ```
31
178
 
32
179
  ### PostgreSQL
33
180
 
34
- PostgreSQL uses the `pg` driver:
181
+ Pass `postgres` as the second argument.
35
182
 
36
- ```javascript
37
- const postgresConfig = {
183
+ ```js
184
+ await connect({
38
185
  host: 'localhost',
39
186
  port: 5432,
40
187
  user: 'postgres',
41
188
  password: 'password',
42
- database: 'mydatabase'
43
- };
189
+ database: 'mydatabase',
190
+ }, 'postgres');
191
+ ```
192
+
193
+ ### Close the connection
44
194
 
45
- await connect(postgresConfig, 'postgres');
195
+ ```js
196
+ await logout();
46
197
  ```
47
198
 
48
- Supported dialects are `mysql` and `postgres`.
199
+ ---
49
200
 
50
- | Dialect | Identifier quotes | Query placeholders | Boolean defaults |
51
- |---------|-------------------|--------------------|------------------|
52
- | MySQL | Backticks | `?` | `1` / `0` |
53
- | PostgreSQL | Double quotes | `$1`, `$2`, ... | `TRUE` / `FALSE` |
201
+ ## Schemas
54
202
 
55
- `logout()` closes the active connection.
203
+ `Schema` describes the desired structure of a SQL table. A field may be declared with a JavaScript constructor, a SQL-connector type alias, or an object containing a type name.
56
204
 
57
- ```javascript
58
- await logout();
205
+ ```js
206
+ const schema = new Schema({
207
+ name: { type: String, length: 120, required: true },
208
+ age: { type: Number },
209
+ active: { type: Boolean, default: true },
210
+ });
59
211
  ```
60
212
 
61
- ### Example
213
+ ### Schema field options
214
+
215
+ | Property | Type | Description |
216
+ |---|---|---|
217
+ | `type` | constructor / string / `{ name }` | Field type. |
218
+ | `length` | `number` | Length for length-based types. |
219
+ | `precision` | `number` | Decimal precision. |
220
+ | `scale` | `number` | Decimal scale. |
221
+ | `required` | `boolean` | Adds `NOT NULL`. |
222
+ | `default` | any | SQL column default. |
223
+ | `unique` | `boolean` | Adds `UNIQUE`. |
224
+ | `primary_key` | `boolean` | Adds `PRIMARY KEY`. |
225
+ | `auto_increment` | `boolean` | Enables auto-increment / identity behavior depending on dialect. |
226
+ | `enum` | `string[]` | Allowed values. |
227
+ | `foreignKey` | `string` | Foreign-key target such as `users(id)` or `users.id`. |
228
+ | `oldname` | `string` | Existing column name used by migrations to request an explicit rename. |
229
+ | `customize` | `string` | Additional SQL definition appended to the column. |
230
+
231
+ > Do not combine `primary_key: true` and `unique: true` on the same field. A primary key is already unique and non-null.
232
+
233
+ ### Types
234
+
235
+ `sqlTypeMap` exposes the connector's supported SQL type aliases:
236
+
237
+ | Alias | SQL type | Typical JavaScript value |
238
+ |---|---|---|
239
+ | `String` | `VARCHAR` | `string` |
240
+ | `Char` | `CHAR` | `string` |
241
+ | `Number` | `INT` | `number` |
242
+ | `SmallInt` | `SMALLINT` | `number` |
243
+ | `BigInt` | `BIGINT` | `number` / `bigint` |
244
+ | `Decimal` | `DECIMAL` | `number` / decimal string |
245
+ | `Boolean` | `BOOLEAN` | `boolean` |
246
+ | `Date` | `DATETIME` | `Date` |
247
+ | `DateTime` | `DATETIME` | `Date` |
248
+ | `Timestamp` | `TIMESTAMP` | `Date` |
249
+ | `Float` | `FLOAT` | `number` |
250
+ | `Double` | `DOUBLE` | `number` |
251
+ | `Text` | `TEXT` | `string` |
252
+ | `Blob` | `BLOB` | `Buffer` / binary data |
253
+ | `Binary` | `VARBINARY` | binary data |
254
+ | `Uuid` | `CHAR` | `string` |
255
+ | `Object` | `JSON` | object |
256
+ | `Array` | `VARCHAR` | array / serialized value |
257
+ | `Now` | `NOW()` | SQL temporal expression* |
258
+ | `CurrentTimestamp` | `CURRENT_TIMESTAMP` | SQL temporal expression* |
259
+
260
+ For `Now` and `CurrentTimestamp`, the mapped values are SQL expressions/constants and are primarily intended for defaults; they are not storage column types.
261
+
262
+ Example:
62
263
 
63
264
  ```js
64
- const { connect: dbConnect, client, Model } = require("@mlagie/sql-connector");
265
+ const { sqlTypeMap } = require('@mlagie/sql-connector');
266
+
267
+ console.log(sqlTypeMap.String); // VARCHAR
268
+ console.log(sqlTypeMap.CurrentTimestamp); // CURRENT_TIMESTAMP
269
+ ```
270
+
271
+ The schema API also accepts the JavaScript constructors `String`, `Number`, `Boolean`, `Date`, `Object` and `Array`.
272
+
273
+ ### Length, precision and scale
65
274
 
275
+ Length-based types support `length`:
66
276
 
67
- await dbConnect({
68
- host: 'localhost',
69
- port: 3306,
70
- user: 'root',
71
- password: 'password',
72
- database: 'mydatabase',
73
- connectionLimit: 2,
74
- multipleStatements: true,
75
- idleTimeout: 10000,
76
- typeCast: true,
77
- }).then(() => { Logger.client("- connected to the database") }).catch(error => {
78
- console.error(error);
79
- process.exit();
277
+ ```js
278
+ const schema = new Schema({
279
+ code: { type: String, length: 32 },
280
+ fixed_code: { type: 'CHAR', length: 8 },
281
+ binary_code: { type: 'VARBINARY', length: 64 },
80
282
  });
81
283
  ```
82
284
 
83
- ## Schema
84
-
85
- `Schema` describes the structure of a table. Each field can use the following properties.
285
+ For decimal values:
86
286
 
87
- | Property | Type | Description |
88
- |----------------|----------------------------------|------------------------|
89
- | type | `SqlType` or `{ name: SqlType }` | SQL type for the field |
90
- | length | `number` | Maximum length |
91
- | required | `boolean` | Not null constraint |
92
- | default | `any` | Default value |
93
- | unique | `boolean` | Unique constraint |
94
- | auto_increment | `boolean` | Auto increment |
95
- | foreignKey | `string` | Foreign key reference |
96
- | enum | `string[]` | Allowed values |
97
- | primary_key | `boolean` | Primary key flag |
98
- | customize | `string` | Extra SQL options |
287
+ ```js
288
+ const schema = new Schema({
289
+ amount: {
290
+ type: 'DECIMAL',
291
+ precision: 12,
292
+ scale: 2,
293
+ },
294
+ });
295
+ ```
99
296
 
100
- ### Example Schema creation & Model Creation
297
+ ### Defaults and values
101
298
 
102
- ```javascript
103
- const { Schema, Model, sqlTypeMap } = require("@mlagie/sql-connector");
299
+ The `default` property accepts values that the active dialect can format as SQL defaults:
104
300
 
105
- const userSchema = new Schema({
106
- id: {
107
- type: Number,
108
- auto_increment: true,
109
- primary_key: true
110
- },
111
- group_uuid: {
112
- type: String,
113
- required: true,
114
- primary_key: true,
115
- length: 36
116
- },
117
- email: {
118
- type: String,
119
- length: 255,
120
- unique: true,
121
- required: true
122
- },
123
- status: {
124
- type: String,
125
- enum: ['active', 'inactive', 'pending'],
126
- default: 'pending'
127
- },
128
- uuid: {
129
- type: String,
130
- required: true,
131
- primary_key: true,
132
- length: 36
133
- },
134
- my_uuid: {
135
- type: String,
136
- required: true,
137
- length: 36
138
- },
139
- created_at: {
140
- type: Date,
141
- default: sqlTypeMap.CurrentTimestamp
142
- }
301
+ ```js
302
+ const schema = new Schema({
303
+ active: { type: Boolean, default: true },
304
+ retries: { type: Number, default: 0 },
305
+ note: { type: String, default: 'pending' },
306
+ created_at: { type: Date, default: 'CURRENT_TIMESTAMP' },
307
+ metadata: { type: Object, default: { source: 'api' } },
143
308
  });
309
+ ```
310
+
311
+ Supported default value forms include:
312
+
313
+ - `null`;
314
+ - strings;
315
+ - numbers and `bigint` values;
316
+ - booleans;
317
+ - `Date` instances;
318
+ - objects, serialized as JSON text;
319
+ - temporal SQL values such as `CURRENT_TIMESTAMP`, `NOW` and `NOW()`;
320
+ - functions returning a supported value.
321
+
322
+ `sqlTypeMap.CurrentTimestamp` can also be used as a semantic constant:
323
+
324
+ ```js
325
+ created_at: {
326
+ type: Date,
327
+ default: sqlTypeMap.CurrentTimestamp,
328
+ }
329
+ ```
330
+
331
+ ### Constraints
144
332
 
145
- module.exports = new Model("User", userSchema);
333
+ ```js
334
+ const schema = new Schema({
335
+ id: {
336
+ type: Number,
337
+ primary_key: true,
338
+ auto_increment: true,
339
+ },
340
+ email: {
341
+ type: String,
342
+ required: true,
343
+ unique: true,
344
+ length: 320,
345
+ },
346
+ });
146
347
  ```
147
348
 
148
- Example with multiple foreign keys:
349
+ The connector supports the schema-level concepts `NOT NULL`, `UNIQUE`, `PRIMARY KEY`, `FOREIGN KEY`, `DEFAULT`, `ENUM`, auto-increment and custom SQL options.
350
+
351
+ ### Foreign keys
149
352
 
150
- ```javascript
353
+ ```js
151
354
  const orderSchema = new Schema({
152
- user_id: { type: Number, foreignKey: 'users(id)' },
153
- product_id: { type: Number, foreignKey: 'products.id' }
355
+ user_id: {
356
+ type: Number,
357
+ foreignKey: 'users(id)',
358
+ },
359
+ product_id: {
360
+ type: Number,
361
+ foreignKey: 'products.id',
362
+ },
154
363
  });
155
364
  ```
156
365
 
157
- ## Table synchronization
366
+ `Model.syncAllTables()` sorts models according to their foreign-key dependencies. Cyclic dependencies are rejected instead of being silently reordered.
158
367
 
159
- `Model.syncAllTables()` compares JS schemas with the database and applies only meaningful differences. Foreign-key dependencies are created in the required order, and cyclic dependencies are rejected.
368
+ ---
160
369
 
161
- - New columns are added automatically.
162
- - Foreign keys use the `table(column)` format and may be declared on multiple fields.
370
+ ## Models
163
371
 
164
- ```javascript
165
- await Model.syncAllTables();
372
+ `Model` represents a database table.
373
+
374
+ ```js
375
+ const User = new Model('users', userSchema);
166
376
  ```
167
377
 
168
- Important: do not set both `primary_key: true` and `unique: true` on the same field. A primary key is already unique and not null.
378
+ ### `save`
169
379
 
170
- ## Models
380
+ ```js
381
+ await User.save({
382
+ email: 'user@example.com',
383
+ status: 'active',
384
+ });
385
+ ```
171
386
 
172
- `Model` represents a SQL table.
387
+ `save(data)` inserts the supplied fields. Values are sent through driver parameters rather than interpolated into the SQL statement.
173
388
 
174
- Main methods:
389
+ ### `find`
175
390
 
176
- - `save(data)` Inserts a row
177
- - `find(options)` Retrieves entries from the table.
178
- - `count(filter)` Counts rows
179
- - `customRequest(custom)` Runs a custom SQL query
180
- - `delete(filter)` Deletes a row
181
- - `dropTable()` Drops the table
182
- - `generate_uuid()` Generates a unique UUID
391
+ ```js
392
+ const users = await User.find({
393
+ select: ['id', 'email', { count: 'id', as: 'total' }],
394
+ where: {
395
+ status: { IN: ['active', 'pending'] },
396
+ },
397
+ groupBy: ['id', 'email'],
398
+ orderBy: [
399
+ { field: 'email', direction: 'ASC' },
400
+ ],
401
+ limit: 50,
402
+ offset: 0,
403
+ });
404
+ ```
183
405
 
184
- ```javascript
185
- const userModel = new Model('users', userSchema);
406
+ Supported options:
186
407
 
187
- await Model.syncAllTables();
188
- await userModel.save({ email: 'user@example.com', status: 'active' });
408
+ | Option | Type | Description |
409
+ |---|---|---|
410
+ | `select` | `SelectItem[]` | Columns, `SUM`, `COUNT`, `DISTINCT`, date formatting and aliases. |
411
+ | `where` | structured condition object | Filters rows. Raw SQL strings are rejected. |
412
+ | `groupBy` | array | Columns or date-format expressions. |
413
+ | `having` | structured condition object | Filters grouped results. Raw SQL strings are rejected. |
414
+ | `orderBy` | array | `{ field, direction }` sorting or string columns. |
415
+ | `join` | object | Join configuration. |
416
+ | `limit` | integer | Maximum number of rows. `0` is treated as no limit by the current implementation. |
417
+ | `offset` | integer | Number of rows to skip. |
418
+
419
+ ### `count`
420
+
421
+ ```js
422
+ const total = await User.count({
423
+ status: 'active',
424
+ });
425
+ ```
426
+
427
+ `count(filter)` returns the number of matching rows.
428
+
429
+ ### `delete`
430
+
431
+ ```js
432
+ await User.delete({
433
+ email: 'user@example.com',
434
+ });
435
+ ```
436
+
437
+ The method returns `1` when rows are affected and `0` otherwise.
438
+
439
+ ### `dropTable`
189
440
 
190
- const user = await userModel.find({ where: { email: 'user@example.com' }});
191
- await user[0].delete();
441
+ ```js
442
+ await User.dropTable();
192
443
  ```
193
444
 
194
- ## save function
445
+ Drops the model table.
446
+
447
+ ### `generate_uuid`
448
+
449
+ ```js
450
+ const uuid = await User.generate_uuid();
451
+ const customUuid = await User.generate_uuid('custom_uuid');
452
+ ```
195
453
 
196
- Saves data to the database table.
454
+ The method asks the active dialect for a UUID, checks uniqueness and returns the generated UUID or `null`.
197
455
 
198
- - **Parameters** `data` *(Object)* - The data to insert into the table.
199
- - **Returns** `Promise<Object>` - A promise that resolves with the result of the insertion.
200
- - **Throws** `Error` - Throws an error if the insert fails.
456
+ ### `customRequest`
201
457
 
202
458
  ```js
203
- const User = require("user");
459
+ const result = await User.customRequest(
460
+ 'SELECT id, email FROM users'
461
+ );
462
+ ```
463
+
464
+ Use this escape hatch for SQL features not represented by the structured query API. Because the SQL string is executed as supplied, only use trusted SQL here.
465
+
466
+ ---
467
+
468
+ ## Conditions
204
469
 
205
- async function createUser(email, stat) {
206
- if (!email || !stat) {
207
- console.error("Email & stat is required");
208
- return;
209
- }
210
- await User.save({ email: email, status: stat });
470
+ Conditions are represented as JavaScript objects. Raw `WHERE` and `HAVING` strings are intentionally rejected by the structured query builder.
211
471
 
472
+ ### Simple values
473
+
474
+ ```js
475
+ { status: 'active' }
476
+ ```
477
+
478
+ Generates a parameterized equality comparison.
479
+
480
+ Multiple fields are combined with `AND`:
481
+
482
+ ```js
483
+ {
484
+ status: 'active',
485
+ role: 'admin',
212
486
  }
213
487
  ```
214
488
 
215
- ## find function
489
+ ### Comparison operators
216
490
 
217
- Retrieves entries from the table.
491
+ The supported comparison operators are:
218
492
 
219
- - **Parameters** `options` *(Object)* - Query options.
220
- - **Parameters** `options.select` *(Array<string|SelectAggregation>)* - Fields, aggregations, or transformations to be returned.
221
- - **Parameters** `options.where` *(Object / string)* - Filtering conditions (key/value object or raw string condition).
222
- - **Parameters** `options.groupBy` *(Array<string|GroupAggregation>)* - Fields, columns, or expressions used to group results.
223
- - **Parameters** `options.orderBy` *(Array<string|OrderByOption>)* - Sorting rules.
224
- - **Parameters** `options.join` *(JoinOption / JoinOption[])* - Table join configuration structures.
225
- - **Parameters** `options.limit` *(number)* - Maximum number of results to return.
226
- - **Returns** `Promise<Array<ModelInstance>>`
493
+ | Operator | Example |
494
+ |---|---|
495
+ | `=` | `{ age: { '=': 18 } }` |
496
+ | `!=` | `{ status: { '!=': 'deleted' } }` |
497
+ | `>` | `{ age: { '>': 18 } }` |
498
+ | `<` | `{ age: { '<': 65 } }` |
499
+ | `>=` | `{ age: { '>=': 18 } }` |
500
+ | `<=` | `{ age: { '<=': 65 } }` |
501
+ | `LIKE` | `{ email: { LIKE: '%@example.com' } }` |
502
+ | `NOT LIKE` | `{ email: { 'NOT LIKE': '%@example.com' } }` |
503
+ | `ILIKE` | `{ email: { ILIKE: '%@example.com' } }` |
504
+ | `NOT ILIKE` | `{ email: { 'NOT ILIKE': '%@example.com' } }` |
505
+ | `IN` | `{ status: { IN: ['active', 'pending'] } }` |
506
+ | `NOT IN` | `{ status: { 'NOT IN': ['deleted'] } }` |
507
+ | `BETWEEN` | `{ age: { BETWEEN: [18, 65] } }` |
508
+ | `NOT BETWEEN` | `{ age: { 'NOT BETWEEN': [18, 65] } }` |
227
509
 
228
- ### Advanced `select` Options (`SelectAggregation`)
510
+ `ILIKE` is native on PostgreSQL. On MySQL it falls back to a case-insensitive `LOWER(column) LIKE LOWER(value)` expression.
229
511
 
230
- Each element in the `select` array can be either a standard string (raw column name) or an object providing advanced SQL capabilities and aggregations:
512
+ ### `IN` / `NOT IN`
231
513
 
232
- | Property inside `select` object | Type | Description | Example / Generated SQL |
233
- |---------------------------------|-------------------|------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------|
234
- | `col` | `string` | Selects a plain, unaggregated table column. | `{ col: 'email' }` $\rightarrow$ \`email\` |
235
- | `sum` | `string` | Computes the sum of all numerical values in a column. | `{ sum: 'error' }` $\rightarrow$ `SUM(` \`error\``)` |
236
- | `distinct` | `string` | Applies a DISTINCT constraint on the specified column. | `{ distinct: 'status' }` $\rightarrow$ `DISTINCT` \`status\` |
237
- | `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')` |
238
- | `count` | `string` | Standard row counting (ignores `NULL` values). | `{ count: 'id' }` $\rightarrow$ `COUNT(` \`id\``)` |
239
- | `count` (Array) | `string[]` | Counts unique combinations across multiple columns (COUNT DISTINCT). | `{ count: ['team', 'source'] }` $\rightarrow$ `COUNT( DISTINCT` \`team\``,` \`source\``)` |
240
- | `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)` |
241
- | `as` | `string` | Sets a custom output identifier or aggregation alias (SQL `AS`). | `{ count: 'id', as: 'total' }` $\rightarrow$ `COUNT(` \`id\``) AS` \`total\` |
514
+ ```js
515
+ {
516
+ status: {
517
+ IN: ['active', 'pending'],
518
+ },
519
+ }
520
+ ```
521
+
522
+ Values are parameterized individually.
242
523
 
243
- ### Advanced `groupBy` Options (`GroupAggregation`)
524
+ ```js
525
+ {
526
+ status: {
527
+ 'NOT IN': ['deleted', 'blocked'],
528
+ },
529
+ }
530
+ ```
244
531
 
245
- The `groupBy` option accepts an array that can mix standard strings (raw column names) and advanced configuration objects for secure and parameterized grouping:
532
+ Use arrays for these operators. An empty list is not a meaningful SQL `IN` expression and should not be passed.
246
533
 
247
- | object | Type | Description | Example / Generated SQL |
248
- |--------------|--------------------|---------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------|
249
- | `col` | `string` | Safely targets an unaggregated table column using structural object isolation. | { col: 'role' } -> `role` |
250
- | `dateFormat` | `[string, string]` | Securely groups by a formatted Date column using parameterized inputs (Format: [column, format]). | { dateFormat: ['created_at', '%Y-%m'] } -> DATE_FORMAT( `created_at`, '%Y-%m') |
251
- | `as` | `string` | Targets the specified select alias directly to comply with strict MySQL group structures. | `{ col: 'role', as: 'user_role' }` ->\`user_role\` |
534
+ ### `BETWEEN` / `NOT BETWEEN`
252
535
 
253
- ---
536
+ ```js
537
+ {
538
+ created_at: {
539
+ BETWEEN: [
540
+ new Date('2026-01-01'),
541
+ new Date('2026-12-31'),
542
+ ],
543
+ },
544
+ }
545
+ ```
546
+
547
+ The value must contain exactly two bounds: `[min, max]`.
548
+
549
+ ### `NULL`
254
550
 
255
- ### Complex Structured Options (`orderBy` & `join`)
551
+ A plain `null` value is translated to `IS NULL`:
256
552
 
257
- #### OrderByOption
553
+ ```js
554
+ { deleted_at: null }
555
+ ```
258
556
 
259
- Enables explicit sorting across one or multiple columns:
557
+ Explicit equality and inequality with `null` are normalized as well:
260
558
 
261
- - **field** `(string)`: The target column name to apply sorting on.
262
- - **direction** `('ASC'\|'DESC')`: The sorting direction (Defaults to `'ASC'`).
559
+ ```js
560
+ { deleted_at: { '=': null } }
561
+ { deleted_at: { '!=': null } }
562
+ ```
263
563
 
264
- #### JoinOption
564
+ These become `IS NULL` and `IS NOT NULL`; no placeholder is consumed for the `NULL` literal.
265
565
 
266
- Specifies one or multiple relational database table joins:
566
+ ### `AND` / `OR` / `NOT`
267
567
 
268
- - **table** `(string)`: Target table name to join with.
269
- - **on** `(string)`: Relational equation statement string (e.g., `"ProjectPipelines.project_id = Projects.id"`).
270
- - **alias** `(string)` *(Optional)*: An alternative SQL alias name for the joined table.
271
- - **type** `('INNER'\|'LEFT'\|'RIGHT')` *(Optional)*: SQL join modality (Defaults to `'INNER'`).
568
+ Nested conditions are supported:
272
569
 
273
- ---
570
+ ```js
571
+ {
572
+ AND: [
573
+ { active: true },
574
+ {
575
+ OR: [
576
+ { role: 'admin' },
577
+ { role: 'moderator' },
578
+ ],
579
+ },
580
+ ],
581
+ }
582
+ ```
274
583
 
275
- ## Example find
584
+ Negation accepts one structured condition object:
276
585
 
277
586
  ```js
278
- const User = require("user");
587
+ {
588
+ NOT: {
589
+ status: { IN: ['deleted', 'blocked'] },
590
+ },
591
+ }
592
+ ```
593
+
594
+ Empty `AND` / `OR` arrays and invalid `NOT` values are rejected.
595
+
596
+ ---
597
+
598
+ ## Select, aggregation, grouping and ordering
599
+
600
+ ### Select items
279
601
 
280
- User.find({
602
+ A `select` entry can be a column string or a structured object.
603
+
604
+ | Form | Meaning |
605
+ |---|---|
606
+ | `'email'` | Select a column. |
607
+ | `'*'` | Select all columns. |
608
+ | `{ col: 'email', as: 'address' }` | Column with alias. |
609
+ | `{ sum: 'amount', as: 'total' }` | `SUM(amount)`. |
610
+ | `{ count: 'id', as: 'total' }` | `COUNT(id)`. |
611
+ | `{ count: ['team', 'source'], as: 'groups' }` | Multi-column count expression. |
612
+ | `{ count: { status: 'active' }, as: 'active' }` | Conditional count expression. |
613
+ | `{ distinct: 'status' }` | `DISTINCT status`. |
614
+ | `{ dateFormat: ['created_at', '%Y-%m'], as: 'period' }` | Formatted date expression. |
615
+
616
+ Example de sélection et d'agrégation :
617
+
618
+ ```js
619
+ const stats = await User.find({
281
620
  select: [
282
- { dateFormat: ['date_day', '%Y-%m'], as: 'period' },
283
- { sum: 'error' },
284
- { sum: 'reload' },
621
+ 'status',
622
+ { count: 'id', as: 'total' },
623
+ { sum: 'amount', as: 'revenue' },
285
624
  ],
286
- groupBy: ['period', { dateFormat: ['date_day', '%Y-%m'] }],
287
- orderBy: [{ field: 'period', direction: 'ASC' }],
288
- limit: 10
625
+ where: {
626
+ active: true,
627
+ },
289
628
  });
290
629
  ```
291
630
 
292
- ```js
293
- const User = require("user");
631
+ ### Groupement avec `GROUP BY`
632
+
633
+ `groupBy` s'utilise directement dans les options de `find()`. Il doit correspondre aux colonnes ou expressions qui définissent chaque groupe.
294
634
 
295
- User.find({
635
+ ```js
636
+ const stats = await User.find({
296
637
  select: [
297
- "email"
638
+ 'status',
639
+ { count: 'id', as: 'total' },
640
+ { sum: 'amount', as: 'revenue' },
298
641
  ],
299
- where: {
300
- id: 1
301
- }
302
- })
642
+ groupBy: ['status'],
643
+ });
303
644
  ```
304
645
 
305
- ### Advanced Examples using `find`
646
+ Un élément de `groupBy` peut être une colonne (`'status'`) ou une expression structurée telle que `dateFormat`.
306
647
 
307
- #### 1. Standard, Distinct, and Multi-Column Counting
648
+ ### Filtrer les groupes avec `HAVING`
308
649
 
309
- Count global rows alongside multi-column unique combinations, such as identifying unique team and source pipelines:
650
+ `having` s'applique **après** le groupement et utilise exactement le même format de conditions structurées que `where`. Par exemple, pour conserver uniquement les groupes contenant plus de 10 lignes :
310
651
 
311
652
  ```js
312
- const { MyTable } = require("./models");
313
-
314
- const stats = await MyTable.find({
653
+ const stats = await User.find({
315
654
  select: [
316
- { count: 'id', as: 'total_pipelines' },
317
- { count: ['cteam', 'csource'], as: 'unique_groups' } // Multi-column COUNT DISTINCT
655
+ 'status',
656
+ { count: 'id', as: 'total' },
318
657
  ],
319
- where: { csource: 'web' }
658
+ groupBy: ['status'],
659
+ having: {
660
+ total: { '>': 10 },
661
+ },
320
662
  });
321
663
  ```
322
664
 
323
- #### 2. Conditional Aggregations (Active / Decommissioned KPI Metrics)
665
+ Les chaînes SQL brutes dans `having` sont refusées par le query builder.
324
666
 
325
- 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:
667
+ ### Tri avec `ORDER BY`
326
668
 
327
- ```js
328
- const { MyTable } = require("./models");
669
+ `orderBy` s'ajoute aux mêmes options et permet de trier le résultat final. Il peut notamment trier sur une colonne sélectionnée ou sur un alias d'agrégation :
329
670
 
330
- const ppiStats = await MyTable.find({
671
+ ```js
672
+ const stats = await User.find({
331
673
  select: [
332
- { count: { deletedAt: null }, as: 'active' }, // Counts where deletedAt = NULL
333
- { count: { status: 'SUCCESS' }, as: 'total_success' } // Counts where status = 'SUCCESS'
674
+ 'status',
675
+ { count: 'id', as: 'total' },
676
+ ],
677
+ groupBy: ['status'],
678
+ having: {
679
+ total: { '>': 10 },
680
+ },
681
+ orderBy: [
682
+ { field: 'total', direction: 'DESC' },
683
+ { field: 'status', direction: 'ASC' },
334
684
  ],
335
- where: {
336
- source: 'jenkins',
337
- team: 'GROUP-1'
338
- }
339
685
  });
340
686
  ```
341
687
 
342
- #### 3. Table Joins, Mixed-Type Time Series Grouping, and Multi-Column Sorting
688
+ Seules les directions `ASC` et `DESC` sont acceptées.
343
689
 
344
- An advanced query orchestration combining left table joining, date formatting conversions, and sorting:
690
+ ### Exemple complet : `SELECT` + agrégation + `WHERE` + `GROUP BY` + `HAVING` + `ORDER BY`
345
691
 
346
- ```js
347
- const { MyTable } = require("./models");
692
+ L'exemple suivant montre comment toutes ces clauses se combinent dans une seule requête :
348
693
 
349
- const history = await MyTable.find({
694
+ ```js
695
+ const stats = await User.find({
350
696
  select: [
351
- { col: 'Projects.name', as: 'project_name' },
352
- { dateFormat: ['ProjectPipelines.created_at', '%Y-%m'], as: 'period' },
353
- { count: 'ProjectPipelines.id', as: 'pipelines_count' }
697
+ 'status',
698
+ { count: 'id', as: 'total' },
699
+ { sum: 'amount', as: 'revenue' },
354
700
  ],
355
701
  where: {
356
- status: 'ACTIVE'
702
+ active: true,
357
703
  },
358
- join: {
359
- table: 'Projects',
360
- on: 'ProjectPipelines.project_id = Projects.id',
361
- type: 'LEFT'
704
+ groupBy: ['status'],
705
+ having: {
706
+ total: { '>': 10 },
362
707
  },
363
- groupBy: [
364
- 'project_name',
365
- { dateFormat: ['ProjectPipelines.created_at', '%Y-%m'] }
366
- ],
367
708
  orderBy: [
368
- { field: 'period', direction: 'DESC' },
369
- { field: 'project_name', direction: 'ASC' }
709
+ { field: 'total', direction: 'DESC' },
710
+ ],
711
+ });
712
+ ```
713
+
714
+ Conceptuellement, cette configuration correspond à :
715
+
716
+ ```sql
717
+ SELECT status, COUNT(id) AS total, SUM(amount) AS revenue
718
+ FROM users
719
+ WHERE active = ?
720
+ GROUP BY status
721
+ HAVING total > ?
722
+ ORDER BY total DESC
723
+ ```
724
+
725
+ Les valeurs de `WHERE` et `HAVING` restent paramétrées par le Connector.
726
+
727
+ ---
728
+
729
+ ## Joins
730
+
731
+ The current model API exposes an `INNER JOIN` configuration through `find`:
732
+
733
+ ```js
734
+ const rows = await User.find({
735
+ select: [
736
+ { col: 'users.email', as: 'email' },
737
+ { col: 'profiles.display_name', as: 'name' },
370
738
  ],
371
- limit: 50
739
+ join: {
740
+ table: 'profiles',
741
+ on: 'users.id = profiles.user_id',
742
+ },
372
743
  });
373
744
  ```
374
745
 
375
- ## count function
746
+ The query builder also documents `JoinOption` shapes with `alias` and `type`; however, the current `Model.find()` implementation builds the join from `table` and `on` and emits `INNER JOIN`. Do not rely on `LEFT` / `RIGHT` behavior until the model layer exposes those options end-to-end.
376
747
 
377
- Counts the number of records matching the given filter.
748
+ ---
378
749
 
379
- - **Parameters** `filter` *(Object)* The filter criteria for the query. Should be an object where keys are column names and values are the values to filter by.
380
- - **Returns** `Promise<ModelInstance|number>` - A promise that resolves to a `ModelInstance` if a record is found, or `0` if no records match the filter.
750
+ ## Schema synchronization
381
751
 
382
- ## Example count
752
+ `Model.syncAllTables()` creates model tables in foreign-key dependency order.
383
753
 
384
754
  ```js
385
- const User = require("user");
755
+ await Model.syncAllTables();
756
+ ```
757
+
758
+ It handles:
759
+
760
+ - table creation;
761
+ - multiple foreign keys;
762
+ - dependency ordering;
763
+ - cyclic dependency detection.
764
+
765
+ For controlled schema evolution, prefer the migration API described below.
766
+
767
+ ---
768
+
769
+ ## Migrations
770
+
771
+ The migration subsystem compares the **desired schema from your models** with the **actual database schema**, creates a migration plan, and can execute the resulting DDL.
386
772
 
387
- User.count({
388
- id: id
389
- })
773
+ Migration components are exposed from the package root:
774
+
775
+ ```js
776
+ const {
777
+ Migration,
778
+ MigrationPlan,
779
+ MigrationStore,
780
+ MigrationRunner,
781
+ SchemaDiffer,
782
+ SchemaInspector,
783
+ MySQLDialect,
784
+ PostgreSQLDialect,
785
+ MigrationError,
786
+ DestructiveMigrationError,
787
+ MigrationChecksumError,
788
+ } = require('@mlagie/sql-connector');
390
789
  ```
391
790
 
392
- ## customRequest function
791
+ ### Migration workflow
792
+
793
+ The recommended workflow is:
794
+
795
+ ```text
796
+ Model schemas
797
+ │
798
+ ▼
799
+ Migration.plan()
800
+ │
801
+ ├── inspect current database
802
+ ├── build desired snapshot
803
+ ├── calculate schema differences
804
+ └── generate dialect-specific SQL
805
+ │
806
+ ▼
807
+ MigrationPlan
808
+ │
809
+ ├── inspect changes
810
+ ├── approve()
811
+ └── execute()
812
+ │
813
+ ▼
814
+ Database + migration history + snapshot
815
+ ```
816
+
817
+ Basic usage:
818
+
819
+ ```js
820
+ const migration = new Migration({
821
+ encryptionKey: process.env.SQL_CONNECTOR_MIGRATION_KEY,
822
+ });
823
+
824
+ const plan = await migration.plan({
825
+ name: 'users-schema',
826
+ });
827
+
828
+ console.log(plan.toJSON());
829
+
830
+ if (plan.hasChanges()) {
831
+ plan.approve();
832
+ await plan.execute();
833
+ }
834
+ ```
835
+
836
+ Or use the convenience method:
837
+
838
+ ```js
839
+ await Migration.migrate({
840
+ name: 'users-schema',
841
+ encryptionKey: process.env.SQL_CONNECTOR_MIGRATION_KEY,
842
+ });
843
+ ```
844
+
845
+ ### Migration options
393
846
 
394
- The customRequest function allows you to execute SQL queries that are not supported by sql-connector; this could be in queries where the keywords are not yet implemented.
847
+ `Migration` accepts:
395
848
 
396
- - **Parameters** `custom` *(string)* The custom SQL_request query to execute.
397
- - **Returns** `Promise<void>` A promise that resolves when the query is executed.
398
- - **Throws** `Error` Throws an error if query execution fails.
849
+ | Option | Type | Description |
850
+ |---|---|---|
851
+ | `connection` | driver connection | Existing database connection. Defaults to the active connector connection. |
852
+ | `dialect` | `'mysql' \| 'postgres'` / dialect instance | Database dialect. Defaults to the active connector dialect. |
853
+ | `models` | `Model[]` | Explicit models to compare. Defaults to pending models. |
854
+ | `directory` | `string` | Migration storage directory. Default: `migrations`. |
855
+ | `backupDirectory` | `string` | Encrypted backup directory. |
856
+ | `encryptionKey` | `string` | Secret used for migration and backup encryption. |
857
+ | `retention` | number / duration string | Backup retention. Supports `ms`, `s`, `m`, `h`, `d`, `w`. |
858
+ | `allowDestructiveChanges` | `boolean` | Allows column drops. |
859
+ | `allowDropTables` | `boolean` | Allows table drops. |
399
860
 
400
- ## Example customRequest
861
+ Examples:
401
862
 
402
863
  ```js
403
- const User = require("user");
864
+ new Migration({
865
+ directory: './database/migrations',
866
+ backupDirectory: './database/backups',
867
+ encryptionKey: process.env.SQL_CONNECTOR_MIGRATION_KEY,
868
+ retention: '30d',
869
+ });
870
+ ```
871
+
872
+ ### Migration plans
404
873
 
405
- User.customRequest("SELECT id, email, status
406
- FROM users
407
- WHERE status IN ('active', 'pending')
408
- AND email LIKE '%gmail.com';")
874
+ `Migration.plan()` returns a `MigrationPlan`.
875
+
876
+ ```js
877
+ const plan = await migration.plan({
878
+ id: '20260915_add_status',
879
+ name: 'add status',
880
+ });
409
881
  ```
410
882
 
411
- ## delete function
883
+ Available methods:
884
+
885
+ | Method | Description |
886
+ |---|---|
887
+ | `hasChanges()` | Returns whether operations are pending. |
888
+ | `hasDestructiveChanges()` | Returns whether destructive operations are present. |
889
+ | `destructiveChanges()` | Returns destructive operations. |
890
+ | `approve()` | Marks the plan as approved. |
891
+ | `execute(options)` | Executes the attached plan. |
892
+ | `toJSON()` | Returns a serializable plan representation. |
893
+
894
+ A plan exposes:
895
+
896
+ - `id`;
897
+ - `dialect`;
898
+ - `checksum`;
899
+ - `operations`;
900
+ - generated `sql`;
901
+ - desired `snapshot`;
902
+ - previous/current baseline snapshot.
412
903
 
413
- Deletes an entry from the SQL table that matches the provided filter.
904
+ ### Detected migration operations
414
905
 
415
- - **Parameters** `filter` *(Object)* An object representing the filter conditions for deletion.
416
- - **Returns** `Promise<number>` A promise that resolves to 0 if no rows were deleted, * or to a ModelInstance representing the deleted row.
417
- - **Throws** `Error` Throws an error if the SQL query fails.
906
+ The schema differ can generate:
418
907
 
419
- ## Example delete
908
+ | Operation | Meaning | Destructive |
909
+ |---|---|---:|
910
+ | `createTable` | Creates a missing table. | No |
911
+ | `addColumn` | Adds a missing column. | No |
912
+ | `renameColumn` | Renames a column declared with `oldname`. | No |
913
+ | `alterColumn` | Changes a column definition. | No |
914
+ | `dropColumn` | Removes a column absent from the desired schema. | Yes |
915
+ | `dropTable` | Removes a table absent from the desired schema. | Yes |
916
+
917
+ Renames are explicit. The differ does not guess that two similarly named columns are the same column.
918
+
919
+ Example:
420
920
 
421
921
  ```js
422
- const User = require("user");
922
+ const schema = new Schema({
923
+ email_address: {
924
+ type: String,
925
+ length: 320,
926
+ oldname: 'email',
927
+ },
928
+ });
929
+ ```
423
930
 
424
- User.delete({
425
- email: my@gmail.com
426
- })
931
+ This can produce a `renameColumn` operation from `email` to `email_address`, followed by an `alterColumn` if the definition also changed.
932
+
933
+ ### Destructive changes
934
+
935
+ Column and table removal require explicit authorization.
936
+
937
+ ```js
938
+ const plan = await migration.plan({
939
+ allowDestructiveChanges: true,
940
+ allowDropTables: true,
941
+ });
942
+
943
+ if (plan.hasDestructiveChanges()) {
944
+ console.warn(plan.destructiveChanges());
945
+ }
946
+
947
+ plan.approve();
948
+ await plan.execute({
949
+ allowDestructiveChanges: true,
950
+ });
427
951
  ```
428
952
 
429
- ## dropTable function
953
+ Approval and destructive-change authorization are separate safeguards.
954
+
955
+ ### Checksums
430
956
 
431
- Asynchronously drops a table if it exists in the database.
957
+ Every generated migration receives a SHA-256 checksum derived from its migration id, dialect, operations and generated SQL.
432
958
 
433
- This function constructs a SQL_request query to drop a table with the name specified by the `this.name` property. It then executes the query using a promise-based approach.
434
- If the query is successful, the result is logged to the console.
435
- If an error occurs during the execution of the query, an error message is logged.
959
+ The migration history table is named `sql_connector_migrations`.
436
960
 
437
- - **Returns** `Promise<void>` A promise that resolves when the query execution is complete.
961
+ If an already-applied migration id is found with a different checksum, execution stops instead of silently accepting the mismatch.
438
962
 
439
- ## Example dropTable
963
+ ### Encrypted files
964
+
965
+ Migration SQL is stored as encrypted `.sql.enc` files.
966
+
967
+ Set the key through the constructor:
440
968
 
441
969
  ```js
442
- const User = require("user");
970
+ const migration = new Migration({
971
+ encryptionKey: process.env.SQL_CONNECTOR_MIGRATION_KEY,
972
+ });
973
+ ```
974
+
975
+ Or through the environment:
443
976
 
444
- User.dropTable();
977
+ ```bash
978
+ export SQL_CONNECTOR_MIGRATION_KEY='a-long-random-secret'
445
979
  ```
446
980
 
447
- ## generate_uuid function
981
+ The encryption format uses AES-256-GCM with a derived key. Migration files are written with restrictive filesystem permissions.
448
982
 
449
- Generates a unique UUID for the current model.
450
- This function generates a UUID using the SQL_request `UUID()` function and checks if the generated UUID already exists in the database for the current model. If the UUID is unique, it is returned.
451
- Otherwise, the function resolves to `null`.
983
+ > Never commit `SQL_CONNECTOR_MIGRATION_KEY` or plaintext secrets to source control.
452
984
 
453
- - **Parameters** `string` var_uuid By default, it is set to uuid.
454
- - **Returns** `Promise<string|null>` A promise that resolves to a unique UUID string if successful, or `null` if an error occurs or the UUID is not unique.
455
- - **Throws** `Error` If there is an error executing the SQL_request query.
985
+ ### Backups and retention
456
986
 
457
- ## Example generate_uuid
987
+ If `retention` is configured, affected tables are backed up before migration execution and written as encrypted JSON files.
458
988
 
459
989
  ```js
460
- const User = require("user");
990
+ const migration = new Migration({
991
+ encryptionKey: process.env.SQL_CONNECTOR_MIGRATION_KEY,
992
+ retention: '30d',
993
+ });
994
+ ```
461
995
 
462
- const uuid = await User.generate_uuid();
463
- const my_uuid = await User.generate_uuid("my_uuid");
996
+ Accepted duration units:
997
+
998
+ - `ms`: millisecond
999
+ - `s`: seconde
1000
+ - `m`: minute
1001
+ - `h`: heure
1002
+ - `d`: jour
1003
+ - `w`: semaine
1004
+
1005
+ `Migration.cleanup()` removes expired backup files.
464
1006
 
465
- await User.save({ email: "user@example.com", status: "active", uuid: uuid, my_uuid: my_uuid })
1007
+ Backups are currently data-oriented snapshots. The connector does not automatically restore them.
1008
+
1009
+ ### Migration status
1010
+
1011
+ ```js
1012
+ const migration = new Migration();
1013
+ const status = await migration.status();
1014
+ console.table(status);
466
1015
  ```
467
1016
 
468
- ## Model instances
1017
+ The migration runner maintains the database-side history table and can also inspect the stored migration files.
1018
+
1019
+ ---
469
1020
 
470
- `ModelInstance` represents a row already loaded from the database.
1021
+ ## Model instances
471
1022
 
472
- - `updateOne(model)` updates the row
473
- - `delete(model)` deletes the row using a filter
474
- - `delete()` deletes the instance row
475
- - `customRequest(custom)` runs a custom query
1023
+ Queries return `ModelInstance` objects.
476
1024
 
477
1025
  ```js
478
- const userInstance = await find({ select: "users", where: { email: 'user@example.com' }})[0];
1026
+ const [user] = await User.find({
1027
+ where: { email: 'user@example.com' },
1028
+ });
1029
+
1030
+ await user.updateOne({ status: 'inactive' });
1031
+ await user.delete();
1032
+ ```
479
1033
 
480
- await userInstance.updateOne({ status: 'inactive' });
481
- await userInstance.delete();
1034
+ Available methods include:
1035
+
1036
+ - `updateOne(data)`;
1037
+ - `delete(filter?)`;
1038
+ - `customRequest(sql)`;
1039
+ - `getRecordData()`;
1040
+ - `toJSON()`.
1041
+
1042
+ When a model schema contains primary-key fields, `updateOne()` uses those primary-key values to target the row when possible.
1043
+
1044
+ ---
1045
+
1046
+ ## TypeScript
1047
+
1048
+ The package ships `index.d.ts` declarations.
1049
+
1050
+ ```ts
1051
+ import {
1052
+ Schema,
1053
+ Model,
1054
+ Migration,
1055
+ InferSchema,
1056
+ } from '@mlagie/sql-connector';
1057
+
1058
+ const schema = new Schema({
1059
+ id: {
1060
+ type: Number,
1061
+ primary_key: true,
1062
+ },
1063
+ email: {
1064
+ type: String,
1065
+ required: true,
1066
+ },
1067
+ });
1068
+
1069
+ type User = InferSchema<typeof schema.schemaDict>;
482
1070
  ```
483
1071
 
484
- ## SQL types
1072
+ The declarations cover schema definitions, inferred model records, query conditions, select items and migration classes.
1073
+
1074
+ ---
1075
+
1076
+ ## Security and SQL safety
1077
+
1078
+ The structured query builder is designed to keep identifiers and values separate.
1079
+
1080
+ - `WHERE` values are parameterized.
1081
+ - `HAVING` values are parameterized.
1082
+ - `IN` / `NOT IN` values are parameterized individually.
1083
+ - `BETWEEN` bounds are parameterized individually.
1084
+ - PostgreSQL placeholders are generated as `$1`, `$2`, etc.
1085
+ - MySQL uses `?` placeholders.
1086
+ - Identifiers are validated before dialect-specific escaping.
1087
+ - Raw `WHERE` / `HAVING` strings are rejected by the structured builder.
1088
+
1089
+ `customRequest()` is intentionally outside that protection because it accepts raw SQL. Use it only with trusted query text.
1090
+
1091
+ ---
1092
+
1093
+ ## Dialect matrix
485
1094
 
486
- `sqlTypeMap` exposes the common SQL types.
1095
+ | Feature | MySQL | PostgreSQL |
1096
+ |---|---|---|
1097
+ | Connection driver | `mysql2` | `pg` |
1098
+ | Identifier quotes | backticks | double quotes |
1099
+ | Parameter placeholders | `?` | `$1`, `$2`, ... |
1100
+ | Boolean SQL literals | `1` / `0` | `TRUE` / `FALSE` |
1101
+ | UUID query | `UUID()` | `gen_random_uuid()` |
1102
+ | Transactional DDL | engine-dependent | supported by migration dialect |
487
1103
 
488
- ```javascript
489
- console.log(sqlTypeMap.String); // "VARCHAR"
1104
+ The migration dialect layer is separate from the query dialect layer so schema DDL and runtime queries can evolve independently.
1105
+
1106
+ ---
1107
+
1108
+ ## API exports
1109
+
1110
+ The package root exports:
1111
+
1112
+ ```js
1113
+ {
1114
+ client,
1115
+ connect,
1116
+ logout,
1117
+ Schema,
1118
+ Model,
1119
+ ModelInstance,
1120
+ sqlTypeMap,
1121
+ Migration,
1122
+ MigrationPlan,
1123
+ MigrationStore,
1124
+ MigrationRunner,
1125
+ SchemaDiffer,
1126
+ SchemaInspector,
1127
+ MySQLDialect,
1128
+ PostgreSQLDialect,
1129
+ MigrationError,
1130
+ DestructiveMigrationError,
1131
+ MigrationChecksumError,
1132
+ }
1133
+ ```
1134
+
1135
+ For implementation-level utilities, see the source tree under `src/`.
1136
+
1137
+ ---
1138
+
1139
+ ## Testing
1140
+
1141
+ Install dependencies and run:
1142
+
1143
+ ```bash
1144
+ npm install
1145
+ npm test
490
1146
  ```
491
1147
 
492
- ## Client
1148
+ Coverage:
493
1149
 
494
- `client` is a shared object meant to host reusable application functions.
1150
+ ```bash
1151
+ npm run test:coverage
1152
+ ```
495
1153
 
496
- ```javascript
497
- module.exports = client => {
498
- client.checkServer = () => {
499
- if (server.isLaunch()) {
500
- return 1;
501
- }
1154
+ Security/lint checks:
502
1155
 
503
- return 0;
504
- };
505
- };
1156
+ ```bash
1157
+ npm run security
506
1158
  ```
507
1159
 
508
- ## Summary
1160
+ The migration and query behavior is covered by the test suite, including schema differences, destructive-change protection, condition building, null handling, dialect-specific placeholders and encryption utilities.
1161
+
1162
+ ---
1163
+
1164
+ ## Related documentation
1165
+
1166
+ - [Documentation index](./docs/README.md)
1167
+ - [Schema reference](./docs/SCHEMA.md)
1168
+ - [Conditions and query reference](./docs/QUERY.md)
1169
+ - [Migration reference](./docs/MIGRATIONS.md)
1170
+ - [API reference](./docs/API.md)
1171
+ - [Dialect reference](./docs/DIALECTS.md)
1172
+ - [French README](./docs/fr/README_FR.md)
1173
+ - [Contributing](./CONTRIBUTING.md)
1174
+ - [Security policy](./SECURITY.md)
1175
+ - [Changelog](./CHANGELOG.md)
1176
+
1177
+ ## License
509
1178
 
510
- sql-connector provides a small layer to connect to MySQL, describe schemas, synchronize tables, and manipulate data with typed models.
1179
+ MIT — see [LICENSE](./LICENSE).