@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 +985 -316
- package/docs/API.md +240 -0
- package/docs/DIALECTS.md +56 -0
- package/docs/MIGRATIONS.md +277 -0
- package/docs/QUERY.md +281 -0
- package/docs/README.md +20 -0
- package/docs/SCHEMA.md +167 -0
- package/docs/fr/README_FR.md +549 -305
- package/index.d.ts +2002 -137
- package/index.js +39 -9
- package/package.json +7 -6
- package/src/db/dialects.js +1 -1
- package/src/migration/BackupManager.js +42 -0
- package/src/migration/Migration.js +163 -0
- package/src/migration/MigrationPlan.js +62 -0
- package/src/migration/MigrationRunner.js +194 -0
- package/src/migration/MigrationStore.js +137 -0
- package/src/migration/SchemaDiffer.js +261 -0
- package/src/migration/SchemaInspector.js +44 -0
- package/src/migration/SchemaSnapshot.js +53 -0
- package/src/migration/crypto.js +79 -0
- package/src/migration/dialects/Dialect.js +112 -0
- package/src/migration/dialects/MySQLDialect.js +205 -0
- package/src/migration/dialects/PostgreSQLDialect.js +233 -0
- package/src/migration/dialects/index.js +21 -0
- package/src/migration/errors.js +28 -0
- package/src/migration/index.js +18 -0
- package/src/migration/utils.js +89 -0
- package/src/models/Model.js +8 -2
- package/src/models/Schema.js +20 -20
- package/src/utils/buildQuery/buildQuery.js +51 -11
- package/src/utils/sqlTypeMap.js +8 -0
package/README.md
CHANGED
|
@@ -1,510 +1,1179 @@
|
|
|
1
|
-
# sql-connector
|
|
1
|
+
# @mlagie/sql-connector
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@mlagie/sql-connector)
|
|
4
|
+
[](https://www.npmjs.com/package/@mlagie/sql-connector)
|
|
5
|
+
[](https://github.com/mlagie/sql-connector/blob/main/LICENSE)
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
A small, typed and dialect-aware SQL layer for **MySQL** and **PostgreSQL**.
|
|
7
8
|
|
|
8
|
-
sql-connector
|
|
9
|
+
`@mlagie/sql-connector` provides:
|
|
9
10
|
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
87
|
+
The package includes both MySQL (`mysql2`) and PostgreSQL (`pg`) drivers.
|
|
17
88
|
|
|
18
|
-
|
|
89
|
+
---
|
|
19
90
|
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
181
|
+
Pass `postgres` as the second argument.
|
|
35
182
|
|
|
36
|
-
```
|
|
37
|
-
|
|
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
|
-
|
|
195
|
+
```js
|
|
196
|
+
await logout();
|
|
46
197
|
```
|
|
47
198
|
|
|
48
|
-
|
|
199
|
+
---
|
|
49
200
|
|
|
50
|
-
|
|
51
|
-
|---------|-------------------|--------------------|------------------|
|
|
52
|
-
| MySQL | Backticks | `?` | `1` / `0` |
|
|
53
|
-
| PostgreSQL | Double quotes | `$1`, `$2`, ... | `TRUE` / `FALSE` |
|
|
201
|
+
## Schemas
|
|
54
202
|
|
|
55
|
-
`
|
|
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
|
-
```
|
|
58
|
-
|
|
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
|
-
###
|
|
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 {
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
`Schema` describes the structure of a table. Each field can use the following properties.
|
|
285
|
+
For decimal values:
|
|
86
286
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
###
|
|
297
|
+
### Defaults and values
|
|
101
298
|
|
|
102
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
353
|
+
```js
|
|
151
354
|
const orderSchema = new Schema({
|
|
152
|
-
user_id: {
|
|
153
|
-
|
|
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
|
-
|
|
366
|
+
`Model.syncAllTables()` sorts models according to their foreign-key dependencies. Cyclic dependencies are rejected instead of being silently reordered.
|
|
158
367
|
|
|
159
|
-
|
|
368
|
+
---
|
|
160
369
|
|
|
161
|
-
|
|
162
|
-
- Foreign keys use the `table(column)` format and may be declared on multiple fields.
|
|
370
|
+
## Models
|
|
163
371
|
|
|
164
|
-
|
|
165
|
-
|
|
372
|
+
`Model` represents a database table.
|
|
373
|
+
|
|
374
|
+
```js
|
|
375
|
+
const User = new Model('users', userSchema);
|
|
166
376
|
```
|
|
167
377
|
|
|
168
|
-
|
|
378
|
+
### `save`
|
|
169
379
|
|
|
170
|
-
|
|
380
|
+
```js
|
|
381
|
+
await User.save({
|
|
382
|
+
email: 'user@example.com',
|
|
383
|
+
status: 'active',
|
|
384
|
+
});
|
|
385
|
+
```
|
|
171
386
|
|
|
172
|
-
`
|
|
387
|
+
`save(data)` inserts the supplied fields. Values are sent through driver parameters rather than interpolated into the SQL statement.
|
|
173
388
|
|
|
174
|
-
|
|
389
|
+
### `find`
|
|
175
390
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
185
|
-
const userModel = new Model('users', userSchema);
|
|
406
|
+
Supported options:
|
|
186
407
|
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
191
|
-
await
|
|
441
|
+
```js
|
|
442
|
+
await User.dropTable();
|
|
192
443
|
```
|
|
193
444
|
|
|
194
|
-
|
|
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
|
-
|
|
454
|
+
The method asks the active dialect for a UUID, checks uniqueness and returns the generated UUID or `null`.
|
|
197
455
|
|
|
198
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
489
|
+
### Comparison operators
|
|
216
490
|
|
|
217
|
-
|
|
491
|
+
The supported comparison operators are:
|
|
218
492
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
510
|
+
`ILIKE` is native on PostgreSQL. On MySQL it falls back to a case-insensitive `LOWER(column) LIKE LOWER(value)` expression.
|
|
229
511
|
|
|
230
|
-
|
|
512
|
+
### `IN` / `NOT IN`
|
|
231
513
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
524
|
+
```js
|
|
525
|
+
{
|
|
526
|
+
status: {
|
|
527
|
+
'NOT IN': ['deleted', 'blocked'],
|
|
528
|
+
},
|
|
529
|
+
}
|
|
530
|
+
```
|
|
244
531
|
|
|
245
|
-
|
|
532
|
+
Use arrays for these operators. An empty list is not a meaningful SQL `IN` expression and should not be passed.
|
|
246
533
|
|
|
247
|
-
|
|
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
|
-
|
|
551
|
+
A plain `null` value is translated to `IS NULL`:
|
|
256
552
|
|
|
257
|
-
|
|
553
|
+
```js
|
|
554
|
+
{ deleted_at: null }
|
|
555
|
+
```
|
|
258
556
|
|
|
259
|
-
|
|
557
|
+
Explicit equality and inequality with `null` are normalized as well:
|
|
260
558
|
|
|
261
|
-
|
|
262
|
-
|
|
559
|
+
```js
|
|
560
|
+
{ deleted_at: { '=': null } }
|
|
561
|
+
{ deleted_at: { '!=': null } }
|
|
562
|
+
```
|
|
263
563
|
|
|
264
|
-
|
|
564
|
+
These become `IS NULL` and `IS NOT NULL`; no placeholder is consumed for the `NULL` literal.
|
|
265
565
|
|
|
266
|
-
|
|
566
|
+
### `AND` / `OR` / `NOT`
|
|
267
567
|
|
|
268
|
-
|
|
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
|
-
|
|
584
|
+
Negation accepts one structured condition object:
|
|
276
585
|
|
|
277
586
|
```js
|
|
278
|
-
|
|
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
|
-
|
|
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
|
-
|
|
283
|
-
{
|
|
284
|
-
{ sum: '
|
|
621
|
+
'status',
|
|
622
|
+
{ count: 'id', as: 'total' },
|
|
623
|
+
{ sum: 'amount', as: 'revenue' },
|
|
285
624
|
],
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
625
|
+
where: {
|
|
626
|
+
active: true,
|
|
627
|
+
},
|
|
289
628
|
});
|
|
290
629
|
```
|
|
291
630
|
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
635
|
+
```js
|
|
636
|
+
const stats = await User.find({
|
|
296
637
|
select: [
|
|
297
|
-
|
|
638
|
+
'status',
|
|
639
|
+
{ count: 'id', as: 'total' },
|
|
640
|
+
{ sum: 'amount', as: 'revenue' },
|
|
298
641
|
],
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
})
|
|
642
|
+
groupBy: ['status'],
|
|
643
|
+
});
|
|
303
644
|
```
|
|
304
645
|
|
|
305
|
-
|
|
646
|
+
Un élément de `groupBy` peut être une colonne (`'status'`) ou une expression structurée telle que `dateFormat`.
|
|
306
647
|
|
|
307
|
-
|
|
648
|
+
### Filtrer les groupes avec `HAVING`
|
|
308
649
|
|
|
309
|
-
|
|
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
|
|
313
|
-
|
|
314
|
-
const stats = await MyTable.find({
|
|
653
|
+
const stats = await User.find({
|
|
315
654
|
select: [
|
|
316
|
-
|
|
317
|
-
{ count:
|
|
655
|
+
'status',
|
|
656
|
+
{ count: 'id', as: 'total' },
|
|
318
657
|
],
|
|
319
|
-
|
|
658
|
+
groupBy: ['status'],
|
|
659
|
+
having: {
|
|
660
|
+
total: { '>': 10 },
|
|
661
|
+
},
|
|
320
662
|
});
|
|
321
663
|
```
|
|
322
664
|
|
|
323
|
-
|
|
665
|
+
Les chaînes SQL brutes dans `having` sont refusées par le query builder.
|
|
324
666
|
|
|
325
|
-
|
|
667
|
+
### Tri avec `ORDER BY`
|
|
326
668
|
|
|
327
|
-
|
|
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
|
-
|
|
671
|
+
```js
|
|
672
|
+
const stats = await User.find({
|
|
331
673
|
select: [
|
|
332
|
-
|
|
333
|
-
{ count:
|
|
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
|
-
|
|
688
|
+
Seules les directions `ASC` et `DESC` sont acceptées.
|
|
343
689
|
|
|
344
|
-
|
|
690
|
+
### Exemple complet : `SELECT` + agrégation + `WHERE` + `GROUP BY` + `HAVING` + `ORDER BY`
|
|
345
691
|
|
|
346
|
-
|
|
347
|
-
const { MyTable } = require("./models");
|
|
692
|
+
L'exemple suivant montre comment toutes ces clauses se combinent dans une seule requête :
|
|
348
693
|
|
|
349
|
-
|
|
694
|
+
```js
|
|
695
|
+
const stats = await User.find({
|
|
350
696
|
select: [
|
|
351
|
-
|
|
352
|
-
{
|
|
353
|
-
{
|
|
697
|
+
'status',
|
|
698
|
+
{ count: 'id', as: 'total' },
|
|
699
|
+
{ sum: 'amount', as: 'revenue' },
|
|
354
700
|
],
|
|
355
701
|
where: {
|
|
356
|
-
|
|
702
|
+
active: true,
|
|
357
703
|
},
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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: '
|
|
369
|
-
|
|
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
|
-
|
|
739
|
+
join: {
|
|
740
|
+
table: 'profiles',
|
|
741
|
+
on: 'users.id = profiles.user_id',
|
|
742
|
+
},
|
|
372
743
|
});
|
|
373
744
|
```
|
|
374
745
|
|
|
375
|
-
|
|
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
|
-
|
|
748
|
+
---
|
|
378
749
|
|
|
379
|
-
|
|
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
|
-
|
|
752
|
+
`Model.syncAllTables()` creates model tables in foreign-key dependency order.
|
|
383
753
|
|
|
384
754
|
```js
|
|
385
|
-
|
|
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
|
-
|
|
388
|
-
|
|
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
|
-
|
|
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
|
-
|
|
847
|
+
`Migration` accepts:
|
|
395
848
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
861
|
+
Examples:
|
|
401
862
|
|
|
402
863
|
```js
|
|
403
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
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
|
-
|
|
904
|
+
### Detected migration operations
|
|
414
905
|
|
|
415
|
-
|
|
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
|
-
|
|
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
|
|
922
|
+
const schema = new Schema({
|
|
923
|
+
email_address: {
|
|
924
|
+
type: String,
|
|
925
|
+
length: 320,
|
|
926
|
+
oldname: 'email',
|
|
927
|
+
},
|
|
928
|
+
});
|
|
929
|
+
```
|
|
423
930
|
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
953
|
+
Approval and destructive-change authorization are separate safeguards.
|
|
954
|
+
|
|
955
|
+
### Checksums
|
|
430
956
|
|
|
431
|
-
|
|
957
|
+
Every generated migration receives a SHA-256 checksum derived from its migration id, dialect, operations and generated SQL.
|
|
432
958
|
|
|
433
|
-
|
|
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
|
-
-
|
|
961
|
+
If an already-applied migration id is found with a different checksum, execution stops instead of silently accepting the mismatch.
|
|
438
962
|
|
|
439
|
-
|
|
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
|
|
970
|
+
const migration = new Migration({
|
|
971
|
+
encryptionKey: process.env.SQL_CONNECTOR_MIGRATION_KEY,
|
|
972
|
+
});
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
Or through the environment:
|
|
443
976
|
|
|
444
|
-
|
|
977
|
+
```bash
|
|
978
|
+
export SQL_CONNECTOR_MIGRATION_KEY='a-long-random-secret'
|
|
445
979
|
```
|
|
446
980
|
|
|
447
|
-
|
|
981
|
+
The encryption format uses AES-256-GCM with a derived key. Migration files are written with restrictive filesystem permissions.
|
|
448
982
|
|
|
449
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
990
|
+
const migration = new Migration({
|
|
991
|
+
encryptionKey: process.env.SQL_CONNECTOR_MIGRATION_KEY,
|
|
992
|
+
retention: '30d',
|
|
993
|
+
});
|
|
994
|
+
```
|
|
461
995
|
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1017
|
+
The migration runner maintains the database-side history table and can also inspect the stored migration files.
|
|
1018
|
+
|
|
1019
|
+
---
|
|
469
1020
|
|
|
470
|
-
|
|
1021
|
+
## Model instances
|
|
471
1022
|
|
|
472
|
-
|
|
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
|
|
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
|
-
|
|
481
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
489
|
-
|
|
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
|
-
|
|
1148
|
+
Coverage:
|
|
493
1149
|
|
|
494
|
-
|
|
1150
|
+
```bash
|
|
1151
|
+
npm run test:coverage
|
|
1152
|
+
```
|
|
495
1153
|
|
|
496
|
-
|
|
497
|
-
module.exports = client => {
|
|
498
|
-
client.checkServer = () => {
|
|
499
|
-
if (server.isLaunch()) {
|
|
500
|
-
return 1;
|
|
501
|
-
}
|
|
1154
|
+
Security/lint checks:
|
|
502
1155
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
};
|
|
1156
|
+
```bash
|
|
1157
|
+
npm run security
|
|
506
1158
|
```
|
|
507
1159
|
|
|
508
|
-
|
|
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
|
-
|
|
1179
|
+
MIT — see [LICENSE](./LICENSE).
|