@mlagie/sql-connector 1.3.4 → 1.4.1
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 +7 -0
- package/package.json +1 -1
- package/src/models/Model.js +186 -182
package/README.md
CHANGED
|
@@ -111,6 +111,13 @@ const transferSchema = new Schema({
|
|
|
111
111
|
|
|
112
112
|
## Synchronisation automatique du schéma et gestion des migrations
|
|
113
113
|
|
|
114
|
+
> **⚠️ Important : Synchronisation du schéma et sécurité**
|
|
115
|
+
>
|
|
116
|
+
> Toutes les opérations de synchronisation avancée du schéma (modification de type, contraintes, renommage, suppression de colonnes, etc.) **ne sont effectuées que si vous activez explicitement l’option `{ dangerousSync: true }`** lors de l’appel à `Model.syncAllTables`.
|
|
117
|
+
>
|
|
118
|
+
> Par défaut (`dangerousSync: false`), aucune modification de structure n’est appliquée sur les tables existantes pour garantir la sécurité de vos données en production.
|
|
119
|
+
> Utilisez `dangerousSync` uniquement dans un environnement de développement ou lors de migrations contrôlées.
|
|
120
|
+
|
|
114
121
|
### Ajout, suppression et renommage de colonnes
|
|
115
122
|
|
|
116
123
|
- **Ajout automatique de colonnes**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mlagie/sql-connector",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "Le module sql-connector permet de gérer les connexions à une base de données MySQL, de définir des schémas de tables, et d'interagir avec les données de manière simple et efficace.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
package/src/models/Model.js
CHANGED
|
@@ -155,7 +155,6 @@ class Model {
|
|
|
155
155
|
fs.writeFileSync(backupPath, insertSQL, 'utf-8');
|
|
156
156
|
logs(`Sauvegarde SQL de la table '${dbTable}' effectuée dans '${backupPath}'.`);
|
|
157
157
|
} else {
|
|
158
|
-
fs.writeFileSync(backupPath, '', 'utf-8');
|
|
159
158
|
logs(`Table '${dbTable}' vide, fichier '${backupPath}' créé.`);
|
|
160
159
|
}
|
|
161
160
|
} catch (err) {
|
|
@@ -180,213 +179,215 @@ class Model {
|
|
|
180
179
|
throw err;
|
|
181
180
|
}
|
|
182
181
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
182
|
+
if (dangerousSync) {
|
|
183
|
+
// --- Restauration automatique si backup SQL trouvé ---
|
|
184
|
+
const backupPattern = `./backup_${model.name}_*.sql`;
|
|
185
|
+
const backupFiles = glob.sync(backupPattern).filter(f => !f.endsWith('.ignored'));
|
|
186
|
+
if (backupFiles.length > 0) {
|
|
187
|
+
const latestBackup = backupFiles.sort().reverse()[0];
|
|
188
|
+
const readline = require('readline');
|
|
189
|
+
const rl = readline.createInterface({
|
|
190
|
+
input: process.stdin,
|
|
191
|
+
output: process.stdout
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Demande restauration
|
|
195
|
+
const answer = await new Promise((resolve) => {
|
|
196
|
+
rl.question(
|
|
197
|
+
`Un backup a été trouvé pour la table '${model.name}' (${latestBackup}). Voulez-vous restaurer les données ? (y/N) `,
|
|
198
|
+
(answer) => {
|
|
199
|
+
resolve(answer.trim().toLowerCase());
|
|
200
|
+
}
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (answer === 'y') {
|
|
205
|
+
try {
|
|
206
|
+
const sqlContent = fs.readFileSync(latestBackup, 'utf-8');
|
|
207
|
+
await conn.promise().query(sqlContent);
|
|
208
|
+
await logs(`Backup restauré pour la table '${model.name}'.`);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
error(`Erreur lors de la restauration du backup pour '${model.name}': ${err}`);
|
|
200
211
|
}
|
|
201
|
-
);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
if (answer === 'y') {
|
|
205
|
-
try {
|
|
206
|
-
const sqlContent = fs.readFileSync(latestBackup, 'utf-8');
|
|
207
|
-
await conn.promise().query(sqlContent);
|
|
208
|
-
await logs(`Backup restauré pour la table '${model.name}'.`);
|
|
209
|
-
} catch (err) {
|
|
210
|
-
error(`Erreur lors de la restauration du backup pour '${model.name}': ${err}`);
|
|
211
212
|
}
|
|
213
|
+
|
|
214
|
+
// Toujours demander la suppression après restauration ou non
|
|
215
|
+
await new Promise((resolve) => {
|
|
216
|
+
rl.question(
|
|
217
|
+
`Voulez-vous supprimer le fichier de backup '${latestBackup}' ? (y/N) `,
|
|
218
|
+
(delAnswer) => {
|
|
219
|
+
if (delAnswer.trim().toLowerCase() === 'y') {
|
|
220
|
+
fs.unlinkSync(latestBackup);
|
|
221
|
+
logs(`Backup supprimé : ${latestBackup}`);
|
|
222
|
+
} else {
|
|
223
|
+
// Renomme le fichier pour ne plus proposer la restauration
|
|
224
|
+
fs.renameSync(latestBackup, latestBackup + '.ignored');
|
|
225
|
+
logs(`Backup ignoré pour la table '${model.name}'. Il ne sera plus proposé.`);
|
|
226
|
+
}
|
|
227
|
+
rl.close();
|
|
228
|
+
resolve();
|
|
229
|
+
}
|
|
230
|
+
);
|
|
231
|
+
});
|
|
212
232
|
}
|
|
213
233
|
|
|
214
|
-
//
|
|
215
|
-
await
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
234
|
+
// 2. Synchronise les colonnes (renommage, ajout, suppression)
|
|
235
|
+
const [columns] = await conn.promise().query(
|
|
236
|
+
`SHOW COLUMNS FROM \`${model.name}\``
|
|
237
|
+
);
|
|
238
|
+
let existingCols = columns.map(col => col.Field);
|
|
239
|
+
|
|
240
|
+
for (const [fieldName, field] of Object.entries(model.schema.schemaDict)) {
|
|
241
|
+
if (existingCols.includes(fieldName)) {
|
|
242
|
+
// 1. Récupère infos colonne
|
|
243
|
+
const currentCol = columns.find(col => col.Field === fieldName);
|
|
244
|
+
const [indexes] = await conn.promise().query(
|
|
245
|
+
`SHOW INDEX FROM \`${model.name}\` WHERE Column_name = ?`, [fieldName]
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
// 2. Vérifie les propriétés principales avec plus de précision
|
|
249
|
+
let needModify = false;
|
|
250
|
+
|
|
251
|
+
// --- Vérification du TYPE ---
|
|
252
|
+
const fieldType = getFieldType(field);
|
|
253
|
+
let expectedTypeDef;
|
|
254
|
+
|
|
255
|
+
if (Array.isArray(field.enum) && field.enum.length > 0) {
|
|
256
|
+
// Pour les ENUM, on compare la définition complète
|
|
257
|
+
const enumValues = field.enum.map(v => `'${v.replace(/'/g, "''")}'`).join(", ");
|
|
258
|
+
expectedTypeDef = `enum(${enumValues})`;
|
|
259
|
+
} else {
|
|
260
|
+
// Pour les autres types
|
|
261
|
+
if (!sqlTypeMap[fieldType]) throw new Error(`Field ${fieldName} has unsupported type ${fieldType}.`);
|
|
262
|
+
|
|
263
|
+
expectedTypeDef = sqlTypeMap[fieldType];
|
|
264
|
+
if (sqlTypeMap[fieldType] === "VARCHAR" || sqlTypeMap[fieldType] === "INT") {
|
|
265
|
+
const length = field.length > 0 ? field.length : 255;
|
|
266
|
+
expectedTypeDef += `(${length})`;
|
|
226
267
|
}
|
|
227
|
-
rl.close();
|
|
228
|
-
resolve();
|
|
229
268
|
}
|
|
230
|
-
);
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// 2. Synchronise les colonnes (renommage, ajout, suppression)
|
|
235
|
-
const [columns] = await conn.promise().query(
|
|
236
|
-
`SHOW COLUMNS FROM \`${model.name}\``
|
|
237
|
-
);
|
|
238
|
-
let existingCols = columns.map(col => col.Field);
|
|
239
|
-
|
|
240
|
-
for (const [fieldName, field] of Object.entries(model.schema.schemaDict)) {
|
|
241
|
-
if (existingCols.includes(fieldName)) {
|
|
242
|
-
// 1. Récupère infos colonne
|
|
243
|
-
const currentCol = columns.find(col => col.Field === fieldName);
|
|
244
|
-
const [indexes] = await conn.promise().query(
|
|
245
|
-
`SHOW INDEX FROM \`${model.name}\` WHERE Column_name = ?`, [fieldName]
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
// 2. Vérifie les propriétés principales avec plus de précision
|
|
249
|
-
let needModify = false;
|
|
250
|
-
|
|
251
|
-
// --- Vérification du TYPE ---
|
|
252
|
-
const fieldType = getFieldType(field);
|
|
253
|
-
let expectedTypeDef;
|
|
254
|
-
|
|
255
|
-
if (Array.isArray(field.enum) && field.enum.length > 0) {
|
|
256
|
-
// Pour les ENUM, on compare la définition complète
|
|
257
|
-
const enumValues = field.enum.map(v => `'${v.replace(/'/g, "''")}'`).join(", ");
|
|
258
|
-
expectedTypeDef = `enum(${enumValues})`;
|
|
259
|
-
} else {
|
|
260
|
-
// Pour les autres types
|
|
261
|
-
if (!sqlTypeMap[fieldType]) throw new Error(`Field ${fieldName} has unsupported type ${fieldType}.`);
|
|
262
269
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
}
|
|
270
|
+
// Normalisation pour la comparaison (minuscules, suppression des espaces)
|
|
271
|
+
const normalizeType = (typeStr) => {
|
|
272
|
+
return typeStr.toLowerCase().replace(/\s+/g, '').replace(/`/g, '');
|
|
273
|
+
};
|
|
269
274
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
return typeStr.toLowerCase().replace(/\s+/g, '').replace(/`/g, '');
|
|
273
|
-
};
|
|
275
|
+
const currentTypeNormalized = normalizeType(currentCol.Type);
|
|
276
|
+
const expectedTypeNormalized = normalizeType(expectedTypeDef);
|
|
274
277
|
|
|
275
|
-
|
|
276
|
-
|
|
278
|
+
if (currentTypeNormalized !== expectedTypeNormalized) {
|
|
279
|
+
logs(`Type mismatch for ${fieldName}: DB has '${currentCol.Type}', expected '${expectedTypeDef}'`);
|
|
280
|
+
needModify = true;
|
|
281
|
+
}
|
|
277
282
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
283
|
+
// --- Vérification NULL/NOT NULL ---
|
|
284
|
+
const shouldBeNotNull = field.required === true || field.primary_key === true;
|
|
285
|
+
const isNullableInDB = currentCol.Null === "YES";
|
|
282
286
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
287
|
+
if (shouldBeNotNull && isNullableInDB) {
|
|
288
|
+
logs(`Nullability mismatch for ${fieldName}: DB allows NULL, expected NOT NULL`);
|
|
289
|
+
needModify = true;
|
|
290
|
+
}
|
|
291
|
+
if (!shouldBeNotNull && !isNullableInDB) {
|
|
292
|
+
logs(`Nullability mismatch for ${fieldName}: DB is NOT NULL, expected NULL allowed`);
|
|
293
|
+
needModify = true;
|
|
294
|
+
}
|
|
286
295
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
if (!shouldBeNotNull && !isNullableInDB) {
|
|
292
|
-
logs(`Nullability mismatch for ${fieldName}: DB is NOT NULL, expected NULL allowed`);
|
|
293
|
-
needModify = true;
|
|
294
|
-
}
|
|
296
|
+
// --- Vérification DEFAULT VALUE ---
|
|
297
|
+
const expectedDefault = field.default !== undefined ? field.default : null;
|
|
298
|
+
const currentDefault = currentCol.Default;
|
|
295
299
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
logs(`Default value mismatch for ${fieldName}: DB has NULL, expected '${expectedDefault}'`);
|
|
303
|
-
needModify = true;
|
|
304
|
-
} else if (expectedDefault === null && currentDefault !== null) {
|
|
305
|
-
logs(`Default value mismatch for ${fieldName}: DB has '${currentDefault}', expected NULL`);
|
|
306
|
-
needModify = true;
|
|
307
|
-
} else if (expectedDefault !== null && currentDefault !== null) {
|
|
308
|
-
// Comparaison stringifiée pour éviter les problèmes de type
|
|
309
|
-
if (String(expectedDefault) !== String(currentDefault)) {
|
|
310
|
-
logs(`Default value mismatch for ${fieldName}: DB has '${currentDefault}', expected '${expectedDefault}'`);
|
|
300
|
+
// Gestion spéciale pour les valeurs par défaut
|
|
301
|
+
if (expectedDefault !== null && currentDefault === null) {
|
|
302
|
+
logs(`Default value mismatch for ${fieldName}: DB has NULL, expected '${expectedDefault}'`);
|
|
303
|
+
needModify = true;
|
|
304
|
+
} else if (expectedDefault === null && currentDefault !== null) {
|
|
305
|
+
logs(`Default value mismatch for ${fieldName}: DB has '${currentDefault}', expected NULL`);
|
|
311
306
|
needModify = true;
|
|
307
|
+
} else if (expectedDefault !== null && currentDefault !== null) {
|
|
308
|
+
// Comparaison stringifiée pour éviter les problèmes de type
|
|
309
|
+
if (String(expectedDefault) !== String(currentDefault)) {
|
|
310
|
+
logs(`Default value mismatch for ${fieldName}: DB has '${currentDefault}', expected '${expectedDefault}'`);
|
|
311
|
+
needModify = true;
|
|
312
|
+
}
|
|
312
313
|
}
|
|
313
|
-
}
|
|
314
314
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
315
|
+
// --- Vérification UNIQUE constraint ---
|
|
316
|
+
const isUniqueInDB = indexes.some(idx => idx.Non_unique === 0 && idx.Key_name !== 'PRIMARY');
|
|
317
|
+
if (!!field.unique !== isUniqueInDB) {
|
|
318
|
+
logs(`Unique constraint mismatch for ${fieldName}: DB has unique=${isUniqueInDB}, expected=${!!field.unique}`);
|
|
319
|
+
needModify = true;
|
|
320
|
+
}
|
|
321
321
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
322
|
+
// --- Vérification PRIMARY KEY ---
|
|
323
|
+
const isPrimaryInDB = indexes.some(idx => idx.Key_name === 'PRIMARY');
|
|
324
|
+
if (!!field.primary_key !== isPrimaryInDB) {
|
|
325
|
+
logs(`Primary key mismatch for ${fieldName}: DB has PK=${isPrimaryInDB}, expected=${!!field.primary_key}`);
|
|
326
|
+
needModify = true;
|
|
327
|
+
}
|
|
328
328
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
329
|
+
// --- Vérification AUTO_INCREMENT ---
|
|
330
|
+
const isAutoIncrementInDB = currentCol.Extra.toLowerCase().includes('auto_increment');
|
|
331
|
+
if (!!field.auto_increment !== isAutoIncrementInDB) {
|
|
332
|
+
logs(`Auto increment mismatch for ${fieldName}: DB has AI=${isAutoIncrementInDB}, expected=${!!field.auto_increment}`);
|
|
333
|
+
needModify = true;
|
|
334
|
+
}
|
|
335
335
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
336
|
+
// 3. Si différence, on modifie
|
|
337
|
+
if (needModify) {
|
|
338
|
+
try {
|
|
339
|
+
const newColDef = getColumnDefinition(fieldName, field);
|
|
340
|
+
const alterSQL = `ALTER TABLE \`${model.name}\` MODIFY COLUMN ${newColDef}`;
|
|
341
|
+
logs(`Modifying column ${fieldName}: ${alterSQL}`);
|
|
342
|
+
await conn.promise().query(alterSQL);
|
|
343
|
+
logs(`Column ${fieldName} modified successfully`);
|
|
344
|
+
} catch (err) {
|
|
345
|
+
error(`Error modifying column ${fieldName}: ${err}`);
|
|
346
|
+
}
|
|
346
347
|
}
|
|
347
348
|
}
|
|
349
|
+
// --- Warn si oldName est présent dans le schéma ---
|
|
350
|
+
if (field.oldName) {
|
|
351
|
+
logs(`⚠️ Pensez à retirer la propriété 'oldName' du champ '${fieldName}' dans le schéma JS de '${model.name}' pour éviter des renommages inutiles à l'avenir.`);
|
|
352
|
+
}
|
|
348
353
|
}
|
|
349
|
-
// --- Warn si oldName est présent dans le schéma ---
|
|
350
|
-
if (field.oldName) {
|
|
351
|
-
logs(`⚠️ Pensez à retirer la propriété 'oldName' du champ '${fieldName}' dans le schéma JS de '${model.name}' pour éviter des renommages inutiles à l'avenir.`);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
354
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
355
|
+
// --- Étape 1 : Renommage des colonnes ---
|
|
356
|
+
for (const [fieldName, field] of Object.entries(model.schema.schemaDict)) {
|
|
357
|
+
if (field.oldName && existingCols.includes(field.oldName) && !existingCols.includes(fieldName)) {
|
|
358
|
+
// Génère la définition SQL de la nouvelle colonne
|
|
359
|
+
let colDef = getColumnDefinition(fieldName, field);
|
|
360
|
+
|
|
361
|
+
// Renomme la colonne
|
|
362
|
+
const alterSQL = `ALTER TABLE \`${model.name}\` CHANGE COLUMN \`${field.oldName}\` ${colDef}`;
|
|
363
|
+
await conn.promise().query(alterSQL);
|
|
364
|
+
logs(`Colonne ${field.oldName} renommée en ${fieldName} dans ${model.name}`);
|
|
365
|
+
// Mets à jour existingCols pour la suite
|
|
366
|
+
existingCols = existingCols.map(col => col === field.oldName ? fieldName : col);
|
|
367
|
+
}
|
|
367
368
|
}
|
|
368
|
-
}
|
|
369
369
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
370
|
+
// --- Étape 2 : Ajout des colonnes manquantes ---
|
|
371
|
+
for (const [fieldName, field] of Object.entries(model.schema.schemaDict)) {
|
|
372
|
+
if (!existingCols.includes(fieldName)) {
|
|
373
|
+
let colDef = getColumnDefinition(fieldName, field);
|
|
374
374
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
375
|
+
// Ajoute la colonne
|
|
376
|
+
const alterSQL = `ALTER TABLE \`${model.name}\` ADD COLUMN ${colDef}`;
|
|
377
|
+
await conn.promise().query(alterSQL);
|
|
378
|
+
logs(`Colonne ${fieldName} ajoutée à ${model.name}`);
|
|
379
|
+
existingCols.push(fieldName);
|
|
380
|
+
}
|
|
380
381
|
}
|
|
381
|
-
}
|
|
382
382
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
383
|
+
// --- Étape 3 : Suppression des colonnes orphelines (dangerousSync) ---
|
|
384
|
+
if (dangerousSync) {
|
|
385
|
+
for (const col of existingCols) {
|
|
386
|
+
if (!Object.keys(model.schema.schemaDict).includes(col)) {
|
|
387
|
+
const alterSQL = `ALTER TABLE \`${model.name}\` DROP COLUMN \`${col}\``;
|
|
388
|
+
await conn.promise().query(alterSQL);
|
|
389
|
+
logs(`Colonne ${col} supprimée de ${model.name}`);
|
|
390
|
+
}
|
|
390
391
|
}
|
|
391
392
|
}
|
|
392
393
|
}
|
|
@@ -457,7 +458,7 @@ class Model {
|
|
|
457
458
|
* @param {Object} [options.where] - Filtres (clé/valeur).
|
|
458
459
|
* @param {Array} [options.order] - Ex: [['points', 'DESC']]
|
|
459
460
|
* @param {number} [options.limit] - Limite de résultats.
|
|
460
|
-
* @returns {Promise<Array<
|
|
461
|
+
* @returns {Promise<Array<ModelInstance>>}
|
|
461
462
|
*/
|
|
462
463
|
async findAll(options = {}) {
|
|
463
464
|
const {
|
|
@@ -479,9 +480,12 @@ class Model {
|
|
|
479
480
|
sql_request += ` LIMIT ${limit}`;
|
|
480
481
|
}
|
|
481
482
|
|
|
483
|
+
logs(sql_request)
|
|
484
|
+
|
|
482
485
|
return new Promise((resolve, reject) => {
|
|
483
486
|
getConnexion().promise().query(sql_request).then(([rows]) => {
|
|
484
|
-
|
|
487
|
+
const instances = rows.map(row => new ModelInstance(this.name, row, this.schema));
|
|
488
|
+
resolve(instances);
|
|
485
489
|
}).catch((err) => {
|
|
486
490
|
error(`Error executing findAll: ${err}`);
|
|
487
491
|
resolve([]);
|