@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/docs/QUERY.md
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# Query and condition reference
|
|
2
|
+
|
|
3
|
+
## Table of contents
|
|
4
|
+
|
|
5
|
+
1. [Structured filters](#structured-filters)
|
|
6
|
+
2. [Values](#values)
|
|
7
|
+
3. [Comparison operators](#comparison-operators)
|
|
8
|
+
4. [IN and NOT IN](#in-and-not-in)
|
|
9
|
+
5. [BETWEEN and NOT BETWEEN](#between-and-not-between)
|
|
10
|
+
6. [NULL](#null)
|
|
11
|
+
7. [AND, OR and NOT](#and-or-and-not)
|
|
12
|
+
8. [Select](#select)
|
|
13
|
+
9. [Group by](#group-by)
|
|
14
|
+
10. [Having](#having)
|
|
15
|
+
11. [Order by](#order-by)
|
|
16
|
+
12. [Limit and offset](#limit-and-offset)
|
|
17
|
+
13. [Joins](#joins)
|
|
18
|
+
|
|
19
|
+
## Structured filters
|
|
20
|
+
|
|
21
|
+
`where` and `having` accept structured objects. Raw SQL strings are rejected by the query builder.
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
{
|
|
25
|
+
status: 'active',
|
|
26
|
+
age: { '>=': 18 },
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Fields at the same object level are combined with `AND`.
|
|
31
|
+
|
|
32
|
+
## Values
|
|
33
|
+
|
|
34
|
+
A scalar value means equality:
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
{ status: 'active' }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Values are bound through driver parameters.
|
|
41
|
+
|
|
42
|
+
`null` is special and becomes `IS NULL`:
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
{ deleted_at: null }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Objects that do not contain a recognized operator are treated as values and passed to the driver.
|
|
49
|
+
|
|
50
|
+
## Comparison operators
|
|
51
|
+
|
|
52
|
+
Supported operators:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
=
|
|
56
|
+
!=
|
|
57
|
+
>
|
|
58
|
+
<
|
|
59
|
+
>=
|
|
60
|
+
<=
|
|
61
|
+
LIKE
|
|
62
|
+
NOT LIKE
|
|
63
|
+
ILIKE
|
|
64
|
+
NOT ILIKE
|
|
65
|
+
IN
|
|
66
|
+
NOT IN
|
|
67
|
+
BETWEEN
|
|
68
|
+
NOT BETWEEN
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Examples:
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
{ age: { '>=': 18 } }
|
|
75
|
+
{ email: { LIKE: '%@example.com' } }
|
|
76
|
+
{ email: { 'NOT LIKE': '%@example.com' } }
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## IN and NOT IN
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
{ status: { IN: ['active', 'pending'] } }
|
|
83
|
+
{ status: { 'NOT IN': ['deleted', 'blocked'] } }
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Each item receives its own parameter placeholder.
|
|
87
|
+
|
|
88
|
+
## BETWEEN and NOT BETWEEN
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
{ age: { BETWEEN: [18, 65] } }
|
|
92
|
+
{ age: { 'NOT BETWEEN': [18, 65] } }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The value is a two-element array `[min, max]`.
|
|
96
|
+
|
|
97
|
+
## NULL
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
{ deleted_at: null }
|
|
101
|
+
{ deleted_at: { '=': null } }
|
|
102
|
+
{ deleted_at: { '!=': null } }
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
These compile to `IS NULL` / `IS NOT NULL`.
|
|
106
|
+
|
|
107
|
+
## AND, OR and NOT
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
{
|
|
111
|
+
AND: [
|
|
112
|
+
{ active: true },
|
|
113
|
+
{
|
|
114
|
+
OR: [
|
|
115
|
+
{ role: 'admin' },
|
|
116
|
+
{ role: 'moderator' },
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Negation:
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
{
|
|
127
|
+
NOT: {
|
|
128
|
+
status: { IN: ['deleted', 'blocked'] },
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Empty logical arrays and invalid `NOT` values are rejected.
|
|
134
|
+
|
|
135
|
+
## Select
|
|
136
|
+
|
|
137
|
+
Supported structured select entries include:
|
|
138
|
+
|
|
139
|
+
| Entry | Meaning |
|
|
140
|
+
|---|---|
|
|
141
|
+
| `'email'` | Column |
|
|
142
|
+
| `'*'` | All columns |
|
|
143
|
+
| `{ col: 'email', as: 'address' }` | Column + alias |
|
|
144
|
+
| `{ sum: 'amount', as: 'total' }` | `SUM` |
|
|
145
|
+
| `{ count: 'id', as: 'total' }` | `COUNT` |
|
|
146
|
+
| `{ count: ['team', 'source'] }` | Multi-column count expression |
|
|
147
|
+
| `{ count: { status: 'active' } }` | Conditional count |
|
|
148
|
+
| `{ distinct: 'status' }` | `DISTINCT` |
|
|
149
|
+
| `{ dateFormat: ['created_at', '%Y-%m'] }` | Date formatting |
|
|
150
|
+
|
|
151
|
+
Example:
|
|
152
|
+
|
|
153
|
+
```js
|
|
154
|
+
const stats = await User.find({
|
|
155
|
+
select: [
|
|
156
|
+
'status',
|
|
157
|
+
{ count: 'id', as: 'total' },
|
|
158
|
+
{ sum: 'amount', as: 'revenue' },
|
|
159
|
+
],
|
|
160
|
+
where: { active: true },
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Group by
|
|
165
|
+
|
|
166
|
+
`groupBy` is passed as an option to `find()`:
|
|
167
|
+
|
|
168
|
+
```js
|
|
169
|
+
const stats = await User.find({
|
|
170
|
+
select: [
|
|
171
|
+
'status',
|
|
172
|
+
{ count: 'id', as: 'total' },
|
|
173
|
+
],
|
|
174
|
+
groupBy: ['status'],
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Structured group items can use `col` and `dateFormat` (and the builder accepts `as`).
|
|
179
|
+
|
|
180
|
+
## Having
|
|
181
|
+
|
|
182
|
+
`having` filters grouped results and uses the same structured condition format as `where`:
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
const stats = await User.find({
|
|
186
|
+
select: [
|
|
187
|
+
'status',
|
|
188
|
+
{ count: 'id', as: 'total' },
|
|
189
|
+
],
|
|
190
|
+
groupBy: ['status'],
|
|
191
|
+
having: {
|
|
192
|
+
total: { '>': 10 },
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The condition is applied after `GROUP BY`. Raw `HAVING` strings are rejected.
|
|
198
|
+
|
|
199
|
+
## Order by
|
|
200
|
+
|
|
201
|
+
`orderBy` is also part of the `find()` options and can sort by a selected column or alias:
|
|
202
|
+
|
|
203
|
+
```js
|
|
204
|
+
const stats = await User.find({
|
|
205
|
+
select: [
|
|
206
|
+
'status',
|
|
207
|
+
{ count: 'id', as: 'total' },
|
|
208
|
+
],
|
|
209
|
+
groupBy: ['status'],
|
|
210
|
+
having: {
|
|
211
|
+
total: { '>': 10 },
|
|
212
|
+
},
|
|
213
|
+
orderBy: [
|
|
214
|
+
{ field: 'total', direction: 'DESC' },
|
|
215
|
+
{ field: 'status', direction: 'ASC' },
|
|
216
|
+
],
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Only `ASC` and `DESC` are accepted.
|
|
221
|
+
|
|
222
|
+
## Complete example
|
|
223
|
+
|
|
224
|
+
The following example combines `SELECT`, aggregation, `WHERE`, `GROUP BY`, `HAVING` and `ORDER BY` in one query:
|
|
225
|
+
|
|
226
|
+
```js
|
|
227
|
+
const stats = await User.find({
|
|
228
|
+
select: [
|
|
229
|
+
'status',
|
|
230
|
+
{ count: 'id', as: 'total' },
|
|
231
|
+
{ sum: 'amount', as: 'revenue' },
|
|
232
|
+
],
|
|
233
|
+
where: {
|
|
234
|
+
active: true,
|
|
235
|
+
},
|
|
236
|
+
groupBy: ['status'],
|
|
237
|
+
having: {
|
|
238
|
+
total: { '>': 10 },
|
|
239
|
+
},
|
|
240
|
+
orderBy: [
|
|
241
|
+
{ field: 'total', direction: 'DESC' },
|
|
242
|
+
],
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Conceptually, this produces the following SQL shape:
|
|
247
|
+
|
|
248
|
+
```sql
|
|
249
|
+
SELECT status, COUNT(id) AS total, SUM(amount) AS revenue
|
|
250
|
+
FROM users
|
|
251
|
+
WHERE active = ?
|
|
252
|
+
GROUP BY status
|
|
253
|
+
HAVING total > ?
|
|
254
|
+
ORDER BY total DESC
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`WHERE` and `HAVING` values remain parameterized by the Connector.
|
|
258
|
+
|
|
259
|
+
## Limit and offset
|
|
260
|
+
|
|
261
|
+
```js
|
|
262
|
+
{
|
|
263
|
+
limit: 50,
|
|
264
|
+
offset: 100,
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Both values must be non-negative integers. PostgreSQL uses numbered placeholders for these values.
|
|
269
|
+
|
|
270
|
+
## Joins
|
|
271
|
+
|
|
272
|
+
`Model.find()` currently emits an `INNER JOIN` from `join.table` and `join.on`:
|
|
273
|
+
|
|
274
|
+
```js
|
|
275
|
+
join: {
|
|
276
|
+
table: 'profiles',
|
|
277
|
+
on: 'users.id = profiles.user_id',
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
The `on` expression is SQL text and must therefore be trusted.
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# sql-connector documentation
|
|
2
|
+
|
|
3
|
+
This directory contains the detailed reference documentation for `@mlagie/sql-connector`.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
| Document | Purpose |
|
|
8
|
+
|---|---|
|
|
9
|
+
| [Schema](./SCHEMA.md) | Types, fields, defaults, constraints and foreign keys. |
|
|
10
|
+
| [Queries and conditions](./QUERY.md) | `WHERE`, `HAVING`, operators, values, aggregations, grouping, ordering and joins. |
|
|
11
|
+
| [Migrations](./MIGRATIONS.md) | Planning, schema diffing, approvals, destructive changes, checksums, encryption and backups. |
|
|
12
|
+
| [API](./API.md) | Public exports and method reference. |
|
|
13
|
+
| [Dialects](./DIALECTS.md) | MySQL/PostgreSQL behavior and SQL generation differences. |
|
|
14
|
+
| [French documentation](./fr/README_FR.md) | Documentation française. |
|
|
15
|
+
|
|
16
|
+
## Documentation principles
|
|
17
|
+
|
|
18
|
+
The documentation describes the behavior implemented by the current package rather than every SQL feature that exists in the SQL standard or in an individual database engine.
|
|
19
|
+
|
|
20
|
+
Where an API is intentionally narrower than raw SQL, the supported structured representation is documented explicitly.
|
package/docs/SCHEMA.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Schema reference
|
|
2
|
+
|
|
3
|
+
## Table of contents
|
|
4
|
+
|
|
5
|
+
1. [Schema object](#schema-object)
|
|
6
|
+
2. [Field definition](#field-definition)
|
|
7
|
+
3. [Types](#types)
|
|
8
|
+
4. [Length, precision and scale](#length-precision-and-scale)
|
|
9
|
+
5. [Defaults](#defaults)
|
|
10
|
+
6. [Constraints](#constraints)
|
|
11
|
+
7. [Enums](#enums)
|
|
12
|
+
8. [Foreign keys](#foreign-keys)
|
|
13
|
+
9. [Column renames](#column-renames)
|
|
14
|
+
10. [Custom SQL](#custom-sql)
|
|
15
|
+
|
|
16
|
+
## Schema object
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
const { Schema } = require('@mlagie/sql-connector');
|
|
20
|
+
|
|
21
|
+
const userSchema = new Schema({
|
|
22
|
+
id: { type: Number, primary_key: true, auto_increment: true },
|
|
23
|
+
email: { type: String, required: true, unique: true, length: 320 },
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`Schema` stores the desired field definitions in `schemaDict`.
|
|
28
|
+
|
|
29
|
+
## Field definition
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
{
|
|
33
|
+
type: String,
|
|
34
|
+
length: 255,
|
|
35
|
+
precision: 12,
|
|
36
|
+
scale: 2,
|
|
37
|
+
required: true,
|
|
38
|
+
default: 'active',
|
|
39
|
+
unique: true,
|
|
40
|
+
primary_key: false,
|
|
41
|
+
auto_increment: false,
|
|
42
|
+
enum: ['active', 'inactive'],
|
|
43
|
+
foreignKey: 'users(id)',
|
|
44
|
+
oldname: 'legacy_email',
|
|
45
|
+
customize: 'CHECK (...)'
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Types
|
|
50
|
+
|
|
51
|
+
The built-in map is:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
String -> VARCHAR
|
|
55
|
+
Char -> CHAR
|
|
56
|
+
Number -> INT
|
|
57
|
+
SmallInt -> SMALLINT
|
|
58
|
+
BigInt -> BIGINT
|
|
59
|
+
Decimal -> DECIMAL
|
|
60
|
+
Boolean -> BOOLEAN
|
|
61
|
+
Date -> DATETIME
|
|
62
|
+
Object -> JSON
|
|
63
|
+
Array -> VARCHAR
|
|
64
|
+
Now -> NOW()
|
|
65
|
+
Float -> FLOAT
|
|
66
|
+
Double -> DOUBLE
|
|
67
|
+
Text -> TEXT
|
|
68
|
+
Blob -> BLOB
|
|
69
|
+
Binary -> VARBINARY
|
|
70
|
+
Uuid -> CHAR
|
|
71
|
+
DateTime -> DATETIME
|
|
72
|
+
Timestamp -> TIMESTAMP
|
|
73
|
+
CurrentTimestamp -> CURRENT_TIMESTAMP
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
JavaScript constructors `String`, `Number`, `Boolean`, `Date`, `Object` and `Array` are also accepted by the schema API.
|
|
77
|
+
|
|
78
|
+
## Length, precision and scale
|
|
79
|
+
|
|
80
|
+
Use `length` with `VARCHAR`, `CHAR`, `BINARY` and `VARBINARY`.
|
|
81
|
+
|
|
82
|
+
Use `precision` and `scale` with `DECIMAL`:
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
amount: {
|
|
86
|
+
type: 'DECIMAL',
|
|
87
|
+
precision: 12,
|
|
88
|
+
scale: 2,
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Defaults
|
|
93
|
+
|
|
94
|
+
Defaults may be `null`, strings, numbers, booleans, dates, objects, temporal SQL expressions or functions returning supported values.
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
created_at: {
|
|
98
|
+
type: Date,
|
|
99
|
+
default: 'CURRENT_TIMESTAMP',
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`sqlTypeMap.CurrentTimestamp` is available as a reusable constant.
|
|
104
|
+
|
|
105
|
+
## Constraints
|
|
106
|
+
|
|
107
|
+
Supported field-level constraints:
|
|
108
|
+
|
|
109
|
+
- `required` → `NOT NULL`;
|
|
110
|
+
- `unique` → `UNIQUE`;
|
|
111
|
+
- `primary_key` → `PRIMARY KEY`;
|
|
112
|
+
- `auto_increment` → auto-increment / identity behavior;
|
|
113
|
+
- `foreignKey` → `FOREIGN KEY`;
|
|
114
|
+
- `default` → `DEFAULT`;
|
|
115
|
+
- `enum` → `ENUM` where supported by the generated dialect SQL;
|
|
116
|
+
- `customize` → additional SQL definition.
|
|
117
|
+
|
|
118
|
+
Do not set `primary_key` and `unique` together.
|
|
119
|
+
|
|
120
|
+
## Enums
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
status: {
|
|
124
|
+
type: String,
|
|
125
|
+
enum: ['pending', 'paid', 'cancelled'],
|
|
126
|
+
default: 'pending',
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Enum values are SQL-escaped when the column definition is generated.
|
|
131
|
+
|
|
132
|
+
## Foreign keys
|
|
133
|
+
|
|
134
|
+
The connector accepts both forms:
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
user_id: { type: Number, foreignKey: 'users(id)' }
|
|
138
|
+
user_id: { type: Number, foreignKey: 'users.id' }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`Model.syncAllTables()` orders table creation according to model dependencies and rejects cycles.
|
|
142
|
+
|
|
143
|
+
## Column renames
|
|
144
|
+
|
|
145
|
+
Migrations use `oldname` for explicit renames:
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
email_address: {
|
|
149
|
+
type: String,
|
|
150
|
+
oldname: 'email',
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The differ never guesses a rename from similar names.
|
|
155
|
+
|
|
156
|
+
## Custom SQL
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
amount: {
|
|
160
|
+
type: 'DECIMAL',
|
|
161
|
+
precision: 12,
|
|
162
|
+
scale: 2,
|
|
163
|
+
customize: 'CHECK (amount >= 0)',
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`customize` is appended to the generated column definition. Treat it as trusted SQL.
|