@mlagie/sql-connector 3.0.3 → 3.2.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 +35 -9
- package/package.json +9 -7
- package/src/db/dialects.js +640 -2
- package/src/migration/BackupManager.js +42 -0
- package/src/migration/Migration.js +162 -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/errors.js +28 -0
- package/src/migration/index.js +15 -0
- package/src/migration/utils.js +89 -0
- package/src/models/Model.js +13 -7
- package/src/models/Schema.js +20 -20
- package/src/utils/buildQuery/buildQuery.js +52 -13
- package/src/utils/sqlTypeMap.js +8 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
const { getSafe } = require("../utils/security/safe");
|
|
2
|
+
const { DestructiveMigrationError } = require("./errors");
|
|
3
|
+
|
|
4
|
+
function normalizeType(type) {
|
|
5
|
+
const value = String(type ?? "").toLowerCase();
|
|
6
|
+
|
|
7
|
+
const aliases = {
|
|
8
|
+
string: "varchar",
|
|
9
|
+
varchar: "varchar",
|
|
10
|
+
character_varying: "varchar",
|
|
11
|
+
"character varying": "varchar",
|
|
12
|
+
|
|
13
|
+
integer: "integer",
|
|
14
|
+
int: "integer",
|
|
15
|
+
int4: "integer",
|
|
16
|
+
|
|
17
|
+
bigint: "bigint",
|
|
18
|
+
int8: "bigint",
|
|
19
|
+
|
|
20
|
+
boolean: "boolean",
|
|
21
|
+
bool: "boolean",
|
|
22
|
+
|
|
23
|
+
float: "float",
|
|
24
|
+
double: "float",
|
|
25
|
+
double_precision: "float",
|
|
26
|
+
|
|
27
|
+
date: "date",
|
|
28
|
+
datetime: "datetime",
|
|
29
|
+
timestamp: "timestamp",
|
|
30
|
+
|
|
31
|
+
text: "text"
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return getSafe(aliases, value) || value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeField(field) {
|
|
38
|
+
return {
|
|
39
|
+
type: normalizeType(field.type),
|
|
40
|
+
length: field.length ?? null,
|
|
41
|
+
required: Boolean(field.required),
|
|
42
|
+
default: field.default ?? null,
|
|
43
|
+
unique: Boolean(field.unique),
|
|
44
|
+
auto_increment: Boolean(field.auto_increment),
|
|
45
|
+
primary_key: Boolean(field.primary_key)
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function equivalentField(a, b) {
|
|
50
|
+
return JSON.stringify(normalizeField(a)) === JSON.stringify(normalizeField(b));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
class SchemaDiffer {
|
|
54
|
+
constructor(options = {}) {
|
|
55
|
+
this.allowDestructiveChanges = Boolean(
|
|
56
|
+
options.allowDestructiveChanges
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
this.allowDropTables = Boolean(
|
|
60
|
+
options.allowDropTables
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
diff(desired, current) {
|
|
65
|
+
const operations = [];
|
|
66
|
+
|
|
67
|
+
const desiredTables = desired.tables || {};
|
|
68
|
+
const currentTables = current.tables || {};
|
|
69
|
+
|
|
70
|
+
for (const [tableName, desiredTable] of Object.entries(desiredTables)) {
|
|
71
|
+
const desiredColumns = desiredTable.columns || {};
|
|
72
|
+
|
|
73
|
+
// Table does not exist yet.
|
|
74
|
+
if (!getSafe(currentTables, tableName)) {
|
|
75
|
+
operations.push({
|
|
76
|
+
type: "createTable",
|
|
77
|
+
table: tableName,
|
|
78
|
+
schema: desiredColumns
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const currentTable = getSafe(currentTables, tableName);
|
|
85
|
+
const currentColumns = getSafe(currentTable, "columns") || {};
|
|
86
|
+
|
|
87
|
+
/*
|
|
88
|
+
* First pass:
|
|
89
|
+
* detect explicit renames using `oldname`.
|
|
90
|
+
*
|
|
91
|
+
* We deliberately do not guess renames.
|
|
92
|
+
*/
|
|
93
|
+
const renameOperations = [];
|
|
94
|
+
const renamedFrom = new Set();
|
|
95
|
+
const renamedTo = new Set();
|
|
96
|
+
|
|
97
|
+
for (const [newName, field] of Object.entries(desiredColumns)) {
|
|
98
|
+
const oldName = field?.oldname;
|
|
99
|
+
|
|
100
|
+
if (!oldName) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (oldName === newName) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Column '${newName}' cannot have oldname equal to itself.`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!getSafe(currentColumns, oldName)) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`Column '${oldName}' specified by oldname for '${tableName}.${newName}' does not exist.`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (getSafe(currentColumns, newName)) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Cannot rename '${tableName}.${oldName}' to '${newName}' because '${newName}' already exists.`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (renamedFrom.has(oldName)) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Column '${tableName}.${oldName}' is used by multiple oldname declarations.`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
const operation = {
|
|
128
|
+
type: "renameColumn",
|
|
129
|
+
table: tableName,
|
|
130
|
+
oldName,
|
|
131
|
+
newName
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
renameOperations.push(operation);
|
|
135
|
+
renamedFrom.add(oldName);
|
|
136
|
+
renamedTo.add(newName);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/*
|
|
140
|
+
* Add rename operations first.
|
|
141
|
+
*/
|
|
142
|
+
operations.push(...renameOperations);
|
|
143
|
+
|
|
144
|
+
/*
|
|
145
|
+
* Build a virtual representation of the current schema
|
|
146
|
+
* after applying the renames.
|
|
147
|
+
*
|
|
148
|
+
* This is important because:
|
|
149
|
+
*
|
|
150
|
+
* email -> email_address
|
|
151
|
+
*
|
|
152
|
+
* followed by:
|
|
153
|
+
*
|
|
154
|
+
* email_address VARCHAR(255) -> VARCHAR(512)
|
|
155
|
+
*
|
|
156
|
+
* must generate:
|
|
157
|
+
*
|
|
158
|
+
* RENAME COLUMN
|
|
159
|
+
* ALTER COLUMN
|
|
160
|
+
*/
|
|
161
|
+
const virtualCurrentColumns = {
|
|
162
|
+
...currentColumns
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
for (const operation of renameOperations) {
|
|
166
|
+
virtualCurrentColumns[operation.newName] =
|
|
167
|
+
virtualCurrentColumns[operation.oldName];
|
|
168
|
+
|
|
169
|
+
delete virtualCurrentColumns[operation.oldName];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/*
|
|
173
|
+
* Second pass:
|
|
174
|
+
* detect additions and modifications.
|
|
175
|
+
*/
|
|
176
|
+
for (const [columnName, field] of Object.entries(desiredColumns)) {
|
|
177
|
+
const currentField = getSafe(virtualCurrentColumns, columnName);
|
|
178
|
+
|
|
179
|
+
if (!currentField) {
|
|
180
|
+
operations.push({
|
|
181
|
+
type: "addColumn",
|
|
182
|
+
table: tableName,
|
|
183
|
+
column: columnName,
|
|
184
|
+
field
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!equivalentField(field, currentField)) {
|
|
191
|
+
operations.push({
|
|
192
|
+
type: "alterColumn",
|
|
193
|
+
table: tableName,
|
|
194
|
+
column: columnName,
|
|
195
|
+
field,
|
|
196
|
+
previous: currentField
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/*
|
|
202
|
+
* Third pass:
|
|
203
|
+
* detect removed columns.
|
|
204
|
+
*
|
|
205
|
+
* Renamed columns are excluded because their old name
|
|
206
|
+
* intentionally disappears from the desired schema.
|
|
207
|
+
*/
|
|
208
|
+
for (const currentName of Object.keys(currentColumns)) {
|
|
209
|
+
if (renamedFrom.has(currentName)) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!getSafe(desiredColumns, currentName)) {
|
|
214
|
+
const operation = {
|
|
215
|
+
type: "dropColumn",
|
|
216
|
+
table: tableName,
|
|
217
|
+
column: currentName,
|
|
218
|
+
destructive: true
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
if (!this.allowDestructiveChanges) {
|
|
222
|
+
throw new DestructiveMigrationError(
|
|
223
|
+
`Dropping column '${tableName}.${currentName}' requires allowDestructiveChanges=true.`,
|
|
224
|
+
operation
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
operations.push(operation);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/*
|
|
234
|
+
* Detect removed tables.
|
|
235
|
+
*/
|
|
236
|
+
for (const tableName of Object.keys(currentTables)) {
|
|
237
|
+
if (!getSafe(desiredTables, tableName)) {
|
|
238
|
+
const operation = {
|
|
239
|
+
type: "dropTable",
|
|
240
|
+
table: tableName,
|
|
241
|
+
destructive: true
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
if (!this.allowDropTables) {
|
|
245
|
+
throw new DestructiveMigrationError(
|
|
246
|
+
`Dropping table '${tableName}' requires allowDropTables=true.`,
|
|
247
|
+
operation
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
operations.push(operation);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return operations;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = {
|
|
260
|
+
SchemaDiffer
|
|
261
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const { setSafe } = require("../utils/security/safe");
|
|
2
|
+
|
|
3
|
+
class SchemaInspector {
|
|
4
|
+
constructor(connection, dialect) {
|
|
5
|
+
this.connection = connection;
|
|
6
|
+
this.dialect = dialect;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async query(sql, values = []) {
|
|
10
|
+
return this.dialect.execute(this.connection, sql, values);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async inspect() {
|
|
14
|
+
const queries = this.dialect.introspectionQueries();
|
|
15
|
+
const tableRows = await this.query(queries.tables);
|
|
16
|
+
|
|
17
|
+
const tables = {};
|
|
18
|
+
|
|
19
|
+
for (const row of tableRows || []) {
|
|
20
|
+
const tableName = row.table_name ?? row.TABLE_NAME;
|
|
21
|
+
if (!tableName || tableName === "sql_connector_migrations") continue;
|
|
22
|
+
|
|
23
|
+
const columnRows = await this.query(
|
|
24
|
+
queries.columns,
|
|
25
|
+
[tableName]
|
|
26
|
+
);
|
|
27
|
+
setSafe(tables, tableName, {
|
|
28
|
+
columns: Object.fromEntries(
|
|
29
|
+
(columnRows || []).map(row => {
|
|
30
|
+
const name = row.column_name ?? row.COLUMN_NAME;
|
|
31
|
+
return [name, this.dialect.normalizeDatabaseColumn(row)];
|
|
32
|
+
})
|
|
33
|
+
)
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
version: 1,
|
|
39
|
+
tables
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { SchemaInspector };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const { clone, normalizeType, normalizeDefault, stableStringify, sha256 } = require("./utils");
|
|
2
|
+
|
|
3
|
+
function fieldToSnapshot(field) {
|
|
4
|
+
return {
|
|
5
|
+
type: normalizeType(field),
|
|
6
|
+
length: field.length ?? null,
|
|
7
|
+
required: Boolean(field.required),
|
|
8
|
+
default: normalizeDefault(field.default),
|
|
9
|
+
unique: Boolean(field.unique),
|
|
10
|
+
auto_increment: Boolean(field.auto_increment),
|
|
11
|
+
foreignKey: field.foreignKey ?? null,
|
|
12
|
+
enum: Array.isArray(field.enum) ? [...field.enum] : null,
|
|
13
|
+
primary_key: Boolean(field.primary_key),
|
|
14
|
+
customize: field.customize ?? null,
|
|
15
|
+
oldname: field.oldname ?? null
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function modelsToSnapshot(models) {
|
|
20
|
+
const tables = {};
|
|
21
|
+
|
|
22
|
+
for (const model of models) {
|
|
23
|
+
if (!model || typeof model.name !== "string" || !model.schema?.schemaDict) {
|
|
24
|
+
throw new Error("Migration received an invalid Model.");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
tables[model.name] = {
|
|
28
|
+
columns: Object.fromEntries(
|
|
29
|
+
Object.entries(model.schema.schemaDict).map(([name, field]) => [name, fieldToSnapshot(field)])
|
|
30
|
+
)
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
version: 1,
|
|
36
|
+
tables
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function snapshotChecksum(snapshot) {
|
|
41
|
+
return sha256(stableStringify(snapshot));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function cloneSnapshot(snapshot) {
|
|
45
|
+
return clone(snapshot);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
fieldToSnapshot,
|
|
50
|
+
modelsToSnapshot,
|
|
51
|
+
snapshotChecksum,
|
|
52
|
+
cloneSnapshot
|
|
53
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
const { Buffer } = require("node:buffer");
|
|
3
|
+
const fs = require("fs/promises");
|
|
4
|
+
|
|
5
|
+
const FORMAT = "sql-connector-migration-v1";
|
|
6
|
+
const ALGORITHM = "aes-256-gcm";
|
|
7
|
+
const KEY_LENGTH = 32;
|
|
8
|
+
const IV_LENGTH = 12;
|
|
9
|
+
const SALT_LENGTH = 16;
|
|
10
|
+
const TAG_LENGTH = 16;
|
|
11
|
+
|
|
12
|
+
function deriveKey(secret, salt) {
|
|
13
|
+
if (!secret || typeof secret !== "string") {
|
|
14
|
+
throw new Error(
|
|
15
|
+
"Migration encryption key is missing. Provide encryption.key or SQL_CONNECTOR_MIGRATION_KEY."
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return crypto.scryptSync(secret, salt, KEY_LENGTH);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function encrypt(text, secret) {
|
|
23
|
+
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
24
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
25
|
+
const key = deriveKey(secret, salt);
|
|
26
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
27
|
+
const ciphertext = Buffer.concat([cipher.update(String(text), "utf8"), cipher.final()]);
|
|
28
|
+
const tag = cipher.getAuthTag();
|
|
29
|
+
|
|
30
|
+
return [
|
|
31
|
+
FORMAT,
|
|
32
|
+
`salt=${salt.toString("base64url")}`,
|
|
33
|
+
`iv=${iv.toString("base64url")}`,
|
|
34
|
+
`tag=${tag.toString("base64url")}`,
|
|
35
|
+
`data=${ciphertext.toString("base64url")}`
|
|
36
|
+
].join("\n") + "\n";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function decrypt(payload, secret) {
|
|
40
|
+
const lines = String(payload).trim().split("\n");
|
|
41
|
+
if (lines[0] !== FORMAT) throw new Error("Unsupported encrypted migration format.");
|
|
42
|
+
|
|
43
|
+
const values = Object.fromEntries(lines.slice(1).map(line => {
|
|
44
|
+
const index = line.indexOf("=");
|
|
45
|
+
return [line.slice(0, index), line.slice(index + 1)];
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
const salt = Buffer.from(values.salt, "base64url");
|
|
49
|
+
const iv = Buffer.from(values.iv, "base64url");
|
|
50
|
+
const tag = Buffer.from(values.tag, "base64url");
|
|
51
|
+
const ciphertext = Buffer.from(values.data, "base64url");
|
|
52
|
+
|
|
53
|
+
if (salt.length !== SALT_LENGTH || iv.length !== IV_LENGTH || tag.length !== TAG_LENGTH) {
|
|
54
|
+
throw new Error("Invalid encrypted migration payload.");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const key = deriveKey(secret, salt);
|
|
58
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
59
|
+
decipher.setAuthTag(tag);
|
|
60
|
+
|
|
61
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function encryptFile(inputPath, outputPath, secret) {
|
|
65
|
+
const content = await fs.readFile(inputPath, "utf8");
|
|
66
|
+
await fs.writeFile(outputPath, encrypt(content, secret), { mode: 0o600 });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function decryptFile(inputPath, secret) {
|
|
70
|
+
return decrypt(await fs.readFile(inputPath, "utf8"), secret);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
FORMAT,
|
|
75
|
+
encrypt,
|
|
76
|
+
decrypt,
|
|
77
|
+
encryptFile,
|
|
78
|
+
decryptFile
|
|
79
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
class MigrationError extends Error {
|
|
2
|
+
constructor(message, code = "MIGRATION_ERROR", details = undefined) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "MigrationError";
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.details = details;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class DestructiveMigrationError extends MigrationError {
|
|
11
|
+
constructor(message, details) {
|
|
12
|
+
super(message, "DESTRUCTIVE_MIGRATION", details);
|
|
13
|
+
this.name = "DestructiveMigrationError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class MigrationChecksumError extends MigrationError {
|
|
18
|
+
constructor(message, details) {
|
|
19
|
+
super(message, "MIGRATION_CHECKSUM_MISMATCH", details);
|
|
20
|
+
this.name = "MigrationChecksumError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
MigrationError,
|
|
26
|
+
DestructiveMigrationError,
|
|
27
|
+
MigrationChecksumError
|
|
28
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const { Migration } = require("./Migration");
|
|
2
|
+
const { MigrationPlan } = require("./MigrationPlan");
|
|
3
|
+
const { MigrationStore } = require("./MigrationStore");
|
|
4
|
+
const { MigrationRunner } = require("./MigrationRunner");
|
|
5
|
+
const { SchemaDiffer } = require("./SchemaDiffer");
|
|
6
|
+
const { SchemaInspector } = require("./SchemaInspector");
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
Migration,
|
|
10
|
+
MigrationPlan,
|
|
11
|
+
MigrationStore,
|
|
12
|
+
MigrationRunner,
|
|
13
|
+
SchemaDiffer,
|
|
14
|
+
SchemaInspector,
|
|
15
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
const { getSafe } = require("../utils/security/safe");
|
|
3
|
+
|
|
4
|
+
function assertIdentifier(value, label = "identifier") {
|
|
5
|
+
if (typeof value !== "string" || !/^[A-Za-z_][A-Za-z0-9_$]*$/.test(value)) {
|
|
6
|
+
throw new Error(`Invalid ${label}: ${value}`);
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function quoteIdentifier(name, quote) {
|
|
12
|
+
assertIdentifier(name);
|
|
13
|
+
return `${quote}${name}${quote}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeType(field) {
|
|
17
|
+
const type = field && field.type;
|
|
18
|
+
if (typeof type === "function") return type.name;
|
|
19
|
+
if (type && typeof type === "object" && typeof type.name === "string") return type.name;
|
|
20
|
+
if (typeof type === "string") return type;
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeDefault(value) {
|
|
25
|
+
if (value === undefined) return undefined;
|
|
26
|
+
if (value === null) return null;
|
|
27
|
+
if (typeof value === "string") return value;
|
|
28
|
+
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
29
|
+
return String(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function stableStringify(value) {
|
|
33
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
34
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
35
|
+
return `{${Object.keys(value).sort().map(k => `${JSON.stringify(k)}:${getSafe(value, k)}`)}.join(",")}}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sha256(value) {
|
|
39
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function migrationId(name = "migration") {
|
|
43
|
+
const now = new Date();
|
|
44
|
+
const stamp = now.toISOString()
|
|
45
|
+
.replace(/[-:TZ.]/g, "")
|
|
46
|
+
.slice(0, 14);
|
|
47
|
+
const safeName = String(name)
|
|
48
|
+
.trim()
|
|
49
|
+
.replace(/[^A-Za-z0-9_-]+/g, "_")
|
|
50
|
+
.replace(/^_+|_+$/g, "")
|
|
51
|
+
.slice(0, 80) || "migration";
|
|
52
|
+
return `${stamp}_${safeName}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseDuration(value) {
|
|
56
|
+
if (value === null || value === false || value === undefined) return null;
|
|
57
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) return value;
|
|
58
|
+
|
|
59
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s|m|h|d|w)$/i.exec(String(value).trim());
|
|
60
|
+
if (!match) throw new Error(`Invalid retention duration: ${value}`);
|
|
61
|
+
|
|
62
|
+
const amount = Number(match[1]);
|
|
63
|
+
const unit = match[2].toLowerCase();
|
|
64
|
+
const factors = {
|
|
65
|
+
ms: 1,
|
|
66
|
+
s: 1000,
|
|
67
|
+
m: 60_000,
|
|
68
|
+
h: 3_600_000,
|
|
69
|
+
d: 86_400_000,
|
|
70
|
+
w: 604_800_000
|
|
71
|
+
};
|
|
72
|
+
return amount * getSafe(factors, unit);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function clone(value) {
|
|
76
|
+
return JSON.parse(JSON.stringify(value));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
assertIdentifier,
|
|
81
|
+
quoteIdentifier,
|
|
82
|
+
normalizeType,
|
|
83
|
+
normalizeDefault,
|
|
84
|
+
stableStringify,
|
|
85
|
+
sha256,
|
|
86
|
+
migrationId,
|
|
87
|
+
parseDuration,
|
|
88
|
+
clone
|
|
89
|
+
};
|
package/src/models/Model.js
CHANGED
|
@@ -5,6 +5,7 @@ const { ModelInstance } = require("./ModelInstance");
|
|
|
5
5
|
const { buildSelect, buildQueryParts } = require("../utils/buildQuery/buildQuery");
|
|
6
6
|
const { getSafe, setSafe } = require("../utils/security/safe");
|
|
7
7
|
const { getDialect } = require("../db/dialects");
|
|
8
|
+
const LENGTH_TYPES = new Set(["VARCHAR", "CHAR", "BINARY", "VARBINARY"]);
|
|
8
9
|
|
|
9
10
|
function getFieldType(field) {
|
|
10
11
|
if (typeof field === "object") {
|
|
@@ -25,16 +26,20 @@ function getColumnDefinition(fieldName, field) {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
const fieldType = getFieldType(field);
|
|
28
|
-
const type =
|
|
29
|
+
const type = getDialect().mapType(fieldType);
|
|
29
30
|
if (!type) throw new Error(`Field ${fieldName} has unsupported type ${fieldType}.`);
|
|
30
31
|
|
|
31
32
|
let colDef;
|
|
32
33
|
if (Array.isArray(field.enum) && field.enum.length > 0) {
|
|
33
34
|
const enumValues = field.enum.map(v => `'${v.replace(/'/g, "''")}'`).join(", ");
|
|
34
35
|
colDef = `ENUM(${enumValues})`;
|
|
36
|
+
} else if (type === "DECIMAL") {
|
|
37
|
+
const precision = field.precision > 0 ? field.precision : 10;
|
|
38
|
+
const scale = field.scale >= 0 ? field.scale : 0;
|
|
39
|
+
colDef = `DECIMAL(${precision}, ${scale})`;
|
|
35
40
|
} else {
|
|
36
|
-
const hasLength = type
|
|
37
|
-
colDef =
|
|
41
|
+
const hasLength = LENGTH_TYPES.has(type) || type === "INT";
|
|
42
|
+
colDef = getDialect().columnType(type, hasLength ? (field.length > 0 ? field.length : 255) : undefined);
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
if (field.required) colDef += ' NOT NULL';
|
|
@@ -43,7 +48,7 @@ function getColumnDefinition(fieldName, field) {
|
|
|
43
48
|
|
|
44
49
|
if (defaultDefinition !== null) colDef += ` ${defaultDefinition}`;
|
|
45
50
|
if (field.unique) colDef += ' UNIQUE';
|
|
46
|
-
if (field.auto_increment) colDef +=
|
|
51
|
+
if (field.auto_increment) colDef += ` ${getDialect().autoIncrement(fieldType)}`;
|
|
47
52
|
if (field.primary_key) colDef += ' PRIMARY KEY'
|
|
48
53
|
if (typeof field.customize === 'string' && field.customize.length != 0) colDef += ` ${field.customize}`;
|
|
49
54
|
return `${getDialect().escape(fieldName)} ${colDef}`;
|
|
@@ -160,10 +165,11 @@ class Model {
|
|
|
160
165
|
return `${getDialect().escape(fieldName)} ENUM(${enumValues})`;
|
|
161
166
|
}
|
|
162
167
|
|
|
163
|
-
const type =
|
|
168
|
+
const type = getDialect().mapType(fieldType);
|
|
164
169
|
|
|
165
170
|
if (!type) throw new Error(`Field ${fieldName} has unsupported type ${field}`);
|
|
166
|
-
if (type
|
|
171
|
+
if (LENGTH_TYPES.has(type)) return `${getDialect().escape(fieldName)} ${type}(${lengthDefault})`;
|
|
172
|
+
if (type === "DECIMAL") return `${getDialect().escape(fieldName)} ${type}(10, 0)`;
|
|
167
173
|
return `${getDialect().escape(fieldName)} ${type}`;
|
|
168
174
|
});
|
|
169
175
|
return `CREATE TABLE IF NOT EXISTS ${getDialect().escape(this.name)} (${columns.join(', ')}${foreignKey.length > 0 ? ", " + foreignKey.join(', ') : ""}) ${getDialect().tableSuffix};`;
|
|
@@ -177,7 +183,7 @@ class Model {
|
|
|
177
183
|
*/
|
|
178
184
|
async save(data) {
|
|
179
185
|
const keys = Object.keys(data);
|
|
180
|
-
const sql_request = `INSERT INTO ${getDialect().escape(this.name)} (${getDialect().escapeIdentifierList(keys)}) VALUES (${keys.map(() =>
|
|
186
|
+
const sql_request = `INSERT INTO ${getDialect().escape(this.name)} (${getDialect().escapeIdentifierList(keys)}) VALUES (${keys.map((_, index) => getDialect().getPlaceholder(index)).join(", ")})`;
|
|
181
187
|
|
|
182
188
|
try {
|
|
183
189
|
const result = await getDialect().execute(getConnexion(), sql_request, Object.values(data));
|
package/src/models/Schema.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class Schema {
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Represents a database schema.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* const transferSchema = new Schema({
|
|
6
|
+
* token: {
|
|
7
|
+
* type: String,
|
|
8
|
+
* length: 50
|
|
9
|
+
* },
|
|
10
|
+
* mdp: {
|
|
11
|
+
* type: String,
|
|
12
|
+
* length: 15
|
|
13
|
+
* }
|
|
14
|
+
* });
|
|
15
|
+
*/
|
|
16
|
+
class Schema {
|
|
17
|
+
constructor(schemaDict) {
|
|
18
|
+
this.schemaDict = schemaDict;
|
|
19
|
+
}
|
|
19
20
|
}
|
|
20
|
-
}
|
|
21
21
|
|
|
22
|
-
module.exports = { Schema }
|
|
22
|
+
module.exports = { Schema }
|