retold-data-service 2.0.21 → 2.0.23
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/.quackage-comprehension-loader.json +19 -0
- package/bin/retold-data-service-clone.js +4 -1
- package/generate-bookstore-comprehension.js +645 -0
- package/package.json +7 -7
- package/source/Retold-Data-Service.js +30 -2
- package/source/services/comprehension-loader/ComprehensionLoader-Command-Load.js +345 -0
- package/source/services/comprehension-loader/ComprehensionLoader-Command-Schema.js +97 -0
- package/source/services/comprehension-loader/ComprehensionLoader-Command-Session.js +221 -0
- package/source/services/comprehension-loader/ComprehensionLoader-Command-WebUI.js +57 -0
- package/source/services/comprehension-loader/Retold-Data-Service-ComprehensionLoader.js +536 -0
- package/source/services/comprehension-loader/pict-app/Pict-Application-ComprehensionLoader-Configuration.json +9 -0
- package/source/services/comprehension-loader/pict-app/Pict-Application-ComprehensionLoader.js +86 -0
- package/source/services/comprehension-loader/pict-app/Pict-ComprehensionLoader-Bundle.js +6 -0
- package/source/services/comprehension-loader/pict-app/providers/Pict-Provider-ComprehensionLoader.js +760 -0
- package/source/services/comprehension-loader/pict-app/views/PictView-ComprehensionLoader-Layout.js +360 -0
- package/source/services/comprehension-loader/pict-app/views/PictView-ComprehensionLoader-Load.js +472 -0
- package/source/services/comprehension-loader/pict-app/views/PictView-ComprehensionLoader-Schema.js +119 -0
- package/source/services/comprehension-loader/pict-app/views/PictView-ComprehensionLoader-Session.js +269 -0
- package/source/services/comprehension-loader/pict-app/views/PictView-ComprehensionLoader-Source.js +330 -0
- package/source/services/comprehension-loader/web/comprehension-loader.js +6794 -0
- package/source/services/comprehension-loader/web/comprehension-loader.js.map +1 -0
- package/source/services/comprehension-loader/web/comprehension-loader.min.js +2 -0
- package/source/services/comprehension-loader/web/comprehension-loader.min.js.map +1 -0
- package/source/services/comprehension-loader/web/index.html +17 -0
- package/source/services/data-cloner/DataCloner-Command-Schema.js +407 -15
- package/source/services/data-cloner/Retold-Data-Service-DataCloner.js +59 -1
- package/source/services/data-cloner/pict-app/Pict-Application-DataCloner.js +1 -0
- package/source/services/data-cloner/pict-app/providers/Pict-Provider-DataCloner.js +125 -5
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Connection.js +18 -8
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Deploy.js +104 -1
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Export.js +1 -1
- package/source/services/data-cloner/pict-app/views/PictView-DataCloner-Layout.js +12 -0
- package/source/services/data-cloner/web/data-cloner.js +201 -139
- package/source/services/data-cloner/web/data-cloner.js.map +1 -1
- package/source/services/data-cloner/web/data-cloner.min.js +1 -1
- package/source/services/data-cloner/web/data-cloner.min.js.map +1 -1
- package/test/RetoldDataService_tests.js +225 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Retold Comprehension Loader</title>
|
|
7
|
+
<style id="PICT-CSS"></style>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="ComprehensionLoader-Application-Container"></div>
|
|
11
|
+
<script src="pict.min.js"></script>
|
|
12
|
+
<script src="comprehension-loader.js"></script>
|
|
13
|
+
<script>
|
|
14
|
+
Pict.safeLoadPictApplication(ComprehensionLoaderApplication, 2);
|
|
15
|
+
</script>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
@@ -15,6 +15,7 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
|
|
|
15
15
|
let tmpPrefix = pDataClonerService.routePrefix;
|
|
16
16
|
|
|
17
17
|
let libFs = require('fs');
|
|
18
|
+
let _ProviderRegistry = require('./DataCloner-ProviderRegistry.js');
|
|
18
19
|
|
|
19
20
|
// POST /clone/schema/fetch
|
|
20
21
|
// Accepts either:
|
|
@@ -318,12 +319,16 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
|
|
|
318
319
|
tmpFable.ProgramConfiguration = {};
|
|
319
320
|
}
|
|
320
321
|
|
|
321
|
-
tmpFable.serviceManager.instantiateServiceProvider('MeadowSync',
|
|
322
|
+
let tmpMeadowSync = tmpFable.serviceManager.instantiateServiceProvider('MeadowSync',
|
|
322
323
|
{
|
|
323
324
|
SyncEntityList: tmpTableNames,
|
|
324
325
|
PageSize: 100,
|
|
325
326
|
SyncDeletedRecords: tmpCloneState.SyncDeletedRecords
|
|
326
327
|
});
|
|
328
|
+
// Ensure the new instance is the default — instantiateServiceProvider
|
|
329
|
+
// only sets the default on the FIRST call for a given service type,
|
|
330
|
+
// so re-deploys would otherwise use the stale MeadowSync.
|
|
331
|
+
tmpFable.serviceManager.setDefaultServiceInstantiation('MeadowSync', tmpMeadowSync.Hash);
|
|
327
332
|
|
|
328
333
|
tmpFable.MeadowSync.loadMeadowSchema(tmpModelObject,
|
|
329
334
|
(pSyncInitError) =>
|
|
@@ -339,30 +344,417 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
|
|
|
339
344
|
// Store the deployed model so sync mode switches can re-create entities
|
|
340
345
|
tmpCloneState.DeployedModelObject = tmpModelObject;
|
|
341
346
|
|
|
342
|
-
|
|
347
|
+
// ---- Schema migration: detect and apply column deltas ----
|
|
348
|
+
let tmpMigrationResults = [];
|
|
343
349
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
(
|
|
350
|
+
let fFinalizeDeploy = () =>
|
|
351
|
+
{
|
|
352
|
+
tmpFable.log.info(`Data Cloner: Loading model for CRUD endpoints...`);
|
|
353
|
+
|
|
354
|
+
// Load the filtered model so CRUD endpoints are available
|
|
355
|
+
tmpFable.RetoldDataServiceMeadowEndpoints.loadModel('RemoteClone', tmpModelObject, tmpCloneState.ConnectionProvider,
|
|
356
|
+
(pLoadError) =>
|
|
357
|
+
{
|
|
358
|
+
if (pLoadError)
|
|
359
|
+
{
|
|
360
|
+
tmpFable.log.error(`Data Cloner: Model load error: ${pLoadError}`);
|
|
361
|
+
}
|
|
362
|
+
else
|
|
363
|
+
{
|
|
364
|
+
tmpFable.log.info(`Data Cloner: CRUD endpoints available for: [${tmpTableNames.join(', ')}]`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
let tmpTotalColumnsAdded = tmpMigrationResults.reduce((pSum, pR) => pSum + pR.ColumnsAdded.length, 0);
|
|
368
|
+
let tmpMessage = `${tmpInitializedEntities.length} / ${tmpTableNames.length} tables deployed.`;
|
|
369
|
+
if (tmpTotalColumnsAdded > 0)
|
|
370
|
+
{
|
|
371
|
+
tmpMessage += ` ${tmpTotalColumnsAdded} column(s) added via schema migration.`;
|
|
372
|
+
}
|
|
373
|
+
tmpMessage += ` meadow-integration sync ready.`;
|
|
374
|
+
|
|
375
|
+
pResponse.send(200,
|
|
376
|
+
{
|
|
377
|
+
Success: true,
|
|
378
|
+
TablesDeployed: tmpTableNames,
|
|
379
|
+
SyncEntities: tmpInitializedEntities,
|
|
380
|
+
MigrationsApplied: tmpMigrationResults,
|
|
381
|
+
Message: tmpMessage
|
|
382
|
+
});
|
|
383
|
+
return fNext();
|
|
384
|
+
});
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// Resolve the active connection provider for introspection and SQL execution
|
|
388
|
+
let tmpProviderName = tmpCloneState.ConnectionProvider;
|
|
389
|
+
let tmpProviderRegistryEntry = _ProviderRegistry[tmpProviderName];
|
|
390
|
+
let tmpActiveProvider = tmpProviderRegistryEntry ? tmpFable[tmpProviderRegistryEntry.serviceName] : null;
|
|
391
|
+
|
|
392
|
+
if (!tmpFable.RetoldDataServiceMigrationManager || !tmpActiveProvider || !tmpActiveProvider.connected)
|
|
393
|
+
{
|
|
394
|
+
// Migration manager or provider not available — skip migration step
|
|
395
|
+
return fFinalizeDeploy();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
let tmpMM = tmpFable.RetoldDataServiceMigrationManager;
|
|
399
|
+
|
|
400
|
+
tmpFable.log.info(`Data Cloner: Checking schema deltas for ${tmpTableNames.length} tables against ${tmpProviderName}...`);
|
|
401
|
+
|
|
402
|
+
tmpFable.Utility.eachLimit(tmpTableNames, 1,
|
|
403
|
+
(pTableName, fNextTable) =>
|
|
404
|
+
{
|
|
405
|
+
if (typeof(tmpActiveProvider.introspectTableColumns) !== 'function')
|
|
406
|
+
{
|
|
407
|
+
// Provider does not support introspection — skip
|
|
408
|
+
return fNextTable();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
tmpActiveProvider.introspectTableColumns(pTableName,
|
|
412
|
+
(pIntrospectError, pActualColumns) =>
|
|
413
|
+
{
|
|
414
|
+
if (pIntrospectError || !Array.isArray(pActualColumns))
|
|
415
|
+
{
|
|
416
|
+
tmpFable.log.warn(`Data Cloner: Could not introspect ${pTableName}: ${pIntrospectError || 'no columns returned'}`);
|
|
417
|
+
return fNextTable();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Build source schema (actual database) in the format SchemaDiff expects
|
|
421
|
+
let tmpSourceSchema = { Tables: [{ TableName: pTableName, Columns: pActualColumns }] };
|
|
422
|
+
|
|
423
|
+
// Build target schema (expected from remote model)
|
|
424
|
+
let tmpTargetSchema = tmpMM.normalizeSchemaForDiff({ Tables: { [pTableName]: tmpModelObject.Tables[pTableName] } });
|
|
425
|
+
|
|
426
|
+
let tmpDiff = tmpMM._schemaDiff.diffSchemas(tmpSourceSchema, tmpTargetSchema);
|
|
427
|
+
|
|
428
|
+
let tmpColumnsAdded = [];
|
|
429
|
+
if (tmpDiff.TablesModified && tmpDiff.TablesModified.length > 0)
|
|
430
|
+
{
|
|
431
|
+
for (let t = 0; t < tmpDiff.TablesModified.length; t++)
|
|
432
|
+
{
|
|
433
|
+
let tmpMod = tmpDiff.TablesModified[t];
|
|
434
|
+
if (Array.isArray(tmpMod.ColumnsAdded))
|
|
435
|
+
{
|
|
436
|
+
for (let c = 0; c < tmpMod.ColumnsAdded.length; c++)
|
|
437
|
+
{
|
|
438
|
+
tmpColumnsAdded.push(tmpMod.ColumnsAdded[c].Column);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (tmpColumnsAdded.length < 1)
|
|
445
|
+
{
|
|
446
|
+
return fNextTable();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Generate provider-appropriate ALTER TABLE statements
|
|
450
|
+
let tmpStatements = tmpMM._migrationGenerator.generateMigrationStatements(tmpDiff, tmpProviderName);
|
|
451
|
+
|
|
452
|
+
tmpFable.log.info(`Data Cloner: Migrating ${pTableName} — adding ${tmpColumnsAdded.length} column(s): [${tmpColumnsAdded.join(', ')}]`);
|
|
453
|
+
|
|
454
|
+
let tmpExecutedStatements = [];
|
|
455
|
+
|
|
456
|
+
// Filter out comment-only lines (e.g. "-- SQLite does not support ALTER COLUMN...")
|
|
457
|
+
// generated by MigrationGenerator for column modifications that can't be
|
|
458
|
+
// applied automatically. We only want to execute actual SQL statements.
|
|
459
|
+
tmpStatements = tmpStatements.filter((pStmt) => !pStmt.trimStart().startsWith('--'));
|
|
460
|
+
|
|
461
|
+
// Execute statements provider-agnostically
|
|
462
|
+
let fExecNext = (pIndex) =>
|
|
463
|
+
{
|
|
464
|
+
if (pIndex >= tmpStatements.length)
|
|
465
|
+
{
|
|
466
|
+
tmpMigrationResults.push(
|
|
467
|
+
{
|
|
468
|
+
Table: pTableName,
|
|
469
|
+
ColumnsAdded: tmpColumnsAdded,
|
|
470
|
+
Statements: tmpExecutedStatements
|
|
471
|
+
});
|
|
472
|
+
return fNextTable();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
let tmpSQL = tmpStatements[pIndex];
|
|
476
|
+
|
|
477
|
+
// SQLite: synchronous execution via better-sqlite3
|
|
478
|
+
if (tmpProviderName === 'SQLite' && tmpActiveProvider.db)
|
|
479
|
+
{
|
|
480
|
+
try
|
|
481
|
+
{
|
|
482
|
+
tmpActiveProvider.db.exec(tmpSQL);
|
|
483
|
+
tmpFable.log.info(`Data Cloner: Migration applied: ${tmpSQL}`);
|
|
484
|
+
tmpExecutedStatements.push(tmpSQL);
|
|
485
|
+
}
|
|
486
|
+
catch (pExecError)
|
|
487
|
+
{
|
|
488
|
+
tmpFable.log.warn(`Data Cloner: Migration failed: ${tmpSQL} — ${pExecError}`);
|
|
489
|
+
}
|
|
490
|
+
return fExecNext(pIndex + 1);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// MySQL / MSSQL / PostgreSQL: async execution via connection pool
|
|
494
|
+
// MySQL pool.query uses callbacks; MSSQL/PostgreSQL pool.query returns a promise.
|
|
495
|
+
if (tmpActiveProvider.pool)
|
|
496
|
+
{
|
|
497
|
+
let tmpQueryResult = tmpActiveProvider.pool.query(tmpSQL,
|
|
498
|
+
(pQueryError) =>
|
|
499
|
+
{
|
|
500
|
+
// Callback-style (MySQL) — only fires if pool.query
|
|
501
|
+
// accepted the callback parameter
|
|
502
|
+
if (pQueryError)
|
|
503
|
+
{
|
|
504
|
+
tmpFable.log.warn(`Data Cloner: Migration failed: ${tmpSQL} — ${pQueryError}`);
|
|
505
|
+
}
|
|
506
|
+
else
|
|
507
|
+
{
|
|
508
|
+
tmpFable.log.info(`Data Cloner: Migration applied: ${tmpSQL}`);
|
|
509
|
+
tmpExecutedStatements.push(tmpSQL);
|
|
510
|
+
}
|
|
511
|
+
return fExecNext(pIndex + 1);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// Promise-style (MSSQL / PostgreSQL) — if query returned a thenable,
|
|
515
|
+
// handle it; the callback above will not fire in this case.
|
|
516
|
+
if (tmpQueryResult && typeof(tmpQueryResult.then) === 'function')
|
|
517
|
+
{
|
|
518
|
+
tmpQueryResult
|
|
519
|
+
.then(() =>
|
|
520
|
+
{
|
|
521
|
+
tmpFable.log.info(`Data Cloner: Migration applied: ${tmpSQL}`);
|
|
522
|
+
tmpExecutedStatements.push(tmpSQL);
|
|
523
|
+
return fExecNext(pIndex + 1);
|
|
524
|
+
})
|
|
525
|
+
.catch((pPromiseError) =>
|
|
526
|
+
{
|
|
527
|
+
tmpFable.log.warn(`Data Cloner: Migration failed: ${tmpSQL} — ${pPromiseError}`);
|
|
528
|
+
return fExecNext(pIndex + 1);
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
else
|
|
533
|
+
{
|
|
534
|
+
tmpFable.log.warn(`Data Cloner: No execution handle available for ${tmpProviderName}; skipping: ${tmpSQL}`);
|
|
535
|
+
return fExecNext(pIndex + 1);
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
fExecNext(0);
|
|
540
|
+
});
|
|
541
|
+
},
|
|
542
|
+
() =>
|
|
347
543
|
{
|
|
348
|
-
if (
|
|
544
|
+
if (tmpMigrationResults.length > 0)
|
|
349
545
|
{
|
|
350
|
-
|
|
546
|
+
let tmpTotalCols = tmpMigrationResults.reduce((pSum, pR) => pSum + pR.ColumnsAdded.length, 0);
|
|
547
|
+
tmpFable.log.info(`Data Cloner: Schema migration complete — ${tmpTotalCols} column(s) added across ${tmpMigrationResults.length} table(s).`);
|
|
351
548
|
}
|
|
352
549
|
else
|
|
353
550
|
{
|
|
354
|
-
tmpFable.log.info(`Data Cloner:
|
|
551
|
+
tmpFable.log.info(`Data Cloner: Schema migration check complete — no deltas detected.`);
|
|
552
|
+
}
|
|
553
|
+
return fFinalizeDeploy();
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// GET /clone/schema/guid-index-audit — Check all deployed tables for missing GUID indices
|
|
559
|
+
pOratorServiceServer.get(`${tmpPrefix}/schema/guid-index-audit`,
|
|
560
|
+
(pRequest, pResponse, fNext) =>
|
|
561
|
+
{
|
|
562
|
+
if (!tmpCloneState.DeployedModelObject)
|
|
563
|
+
{
|
|
564
|
+
pResponse.send(400, { Success: false, Error: 'No schema deployed. Deploy tables first.' });
|
|
565
|
+
return fNext();
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
let tmpProviderName = tmpCloneState.ConnectionProvider;
|
|
569
|
+
let tmpProviderRegistryEntry = _ProviderRegistry[tmpProviderName];
|
|
570
|
+
let tmpActiveProvider = tmpProviderRegistryEntry ? tmpFable[tmpProviderRegistryEntry.serviceName] : null;
|
|
571
|
+
|
|
572
|
+
if (!tmpActiveProvider || !tmpActiveProvider.connected || typeof(tmpActiveProvider.introspectTableIndices) !== 'function')
|
|
573
|
+
{
|
|
574
|
+
pResponse.send(400, { Success: false, Error: 'No connected provider with introspection support available.' });
|
|
575
|
+
return fNext();
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
let tmpModelTables = tmpCloneState.DeployedModelObject.Tables || {};
|
|
579
|
+
let tmpTableNames = Object.keys(tmpModelTables);
|
|
580
|
+
let tmpResults = [];
|
|
581
|
+
let tmpMissingCount = 0;
|
|
582
|
+
|
|
583
|
+
tmpFable.Utility.eachLimit(tmpTableNames, 1,
|
|
584
|
+
(pTableName, fNextTable) =>
|
|
585
|
+
{
|
|
586
|
+
let tmpTableSchema = tmpModelTables[pTableName];
|
|
587
|
+
let tmpColumns = Array.isArray(tmpTableSchema.Columns) ? tmpTableSchema.Columns : [];
|
|
588
|
+
|
|
589
|
+
// Find GUID columns in the expected schema
|
|
590
|
+
let tmpGUIDColumns = tmpColumns.filter((pCol) => pCol.DataType === 'GUID');
|
|
591
|
+
|
|
592
|
+
if (tmpGUIDColumns.length < 1)
|
|
593
|
+
{
|
|
594
|
+
return fNextTable();
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
tmpActiveProvider.introspectTableIndices(pTableName,
|
|
598
|
+
(pError, pActualIndices) =>
|
|
599
|
+
{
|
|
600
|
+
if (pError)
|
|
601
|
+
{
|
|
602
|
+
tmpFable.log.warn(`GUID Index Audit: Could not introspect indices for ${pTableName}: ${pError}`);
|
|
603
|
+
return fNextTable();
|
|
355
604
|
}
|
|
356
605
|
|
|
357
|
-
|
|
606
|
+
let tmpIndices = Array.isArray(pActualIndices) ? pActualIndices : [];
|
|
607
|
+
let tmpTableResult = { Table: pTableName, GUIDColumns: [] };
|
|
608
|
+
|
|
609
|
+
for (let g = 0; g < tmpGUIDColumns.length; g++)
|
|
610
|
+
{
|
|
611
|
+
let tmpColName = tmpGUIDColumns[g].Column;
|
|
612
|
+
let tmpExpectedIndexName = `AK_M_${tmpColName}`;
|
|
613
|
+
|
|
614
|
+
// Check if any index covers this GUID column
|
|
615
|
+
let tmpMatchingIndex = tmpIndices.find((pIdx) =>
|
|
616
|
+
pIdx.Name === tmpExpectedIndexName ||
|
|
617
|
+
(Array.isArray(pIdx.Columns) && pIdx.Columns.indexOf(tmpColName) > -1));
|
|
618
|
+
|
|
619
|
+
let tmpHasIndex = !!tmpMatchingIndex;
|
|
620
|
+
if (!tmpHasIndex)
|
|
358
621
|
{
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
622
|
+
tmpMissingCount++;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
tmpTableResult.GUIDColumns.push(
|
|
626
|
+
{
|
|
627
|
+
Column: tmpColName,
|
|
628
|
+
HasIndex: tmpHasIndex,
|
|
629
|
+
IndexName: tmpMatchingIndex ? tmpMatchingIndex.Name : null
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
tmpResults.push(tmpTableResult);
|
|
634
|
+
return fNextTable();
|
|
365
635
|
});
|
|
636
|
+
},
|
|
637
|
+
() =>
|
|
638
|
+
{
|
|
639
|
+
pResponse.send(200,
|
|
640
|
+
{
|
|
641
|
+
Success: true,
|
|
642
|
+
Tables: tmpResults,
|
|
643
|
+
MissingCount: tmpMissingCount,
|
|
644
|
+
Message: tmpMissingCount > 0
|
|
645
|
+
? `${tmpMissingCount} GUID column(s) across ${tmpResults.filter((pR) => pR.GUIDColumns.some((pC) => !pC.HasIndex)).length} table(s) are missing indices.`
|
|
646
|
+
: 'All GUID columns have indices.'
|
|
647
|
+
});
|
|
648
|
+
return fNext();
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// POST /clone/schema/guid-index-create — Create missing GUID indices
|
|
653
|
+
pOratorServiceServer.post(`${tmpPrefix}/schema/guid-index-create`,
|
|
654
|
+
(pRequest, pResponse, fNext) =>
|
|
655
|
+
{
|
|
656
|
+
if (!tmpCloneState.DeployedModelObject)
|
|
657
|
+
{
|
|
658
|
+
pResponse.send(400, { Success: false, Error: 'No schema deployed. Deploy tables first.' });
|
|
659
|
+
return fNext();
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
let tmpProviderName = tmpCloneState.ConnectionProvider;
|
|
663
|
+
let tmpProviderRegistryEntry = _ProviderRegistry[tmpProviderName];
|
|
664
|
+
let tmpActiveProvider = tmpProviderRegistryEntry ? tmpFable[tmpProviderRegistryEntry.serviceName] : null;
|
|
665
|
+
|
|
666
|
+
if (!tmpActiveProvider || !tmpActiveProvider.connected)
|
|
667
|
+
{
|
|
668
|
+
pResponse.send(400, { Success: false, Error: 'No connected provider available.' });
|
|
669
|
+
return fNext();
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// The schema provider has generateCreateIndexStatements and createIndex
|
|
673
|
+
let tmpSchemaProvider = tmpActiveProvider.schemaProvider || tmpActiveProvider;
|
|
674
|
+
|
|
675
|
+
if (typeof(tmpSchemaProvider.generateCreateIndexStatements) !== 'function' ||
|
|
676
|
+
typeof(tmpSchemaProvider.createIndex) !== 'function')
|
|
677
|
+
{
|
|
678
|
+
pResponse.send(400, { Success: false, Error: `Provider ${tmpProviderName} does not support index creation.` });
|
|
679
|
+
return fNext();
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
let tmpBody = pRequest.body || {};
|
|
683
|
+
let tmpFilterTables = Array.isArray(tmpBody.Tables) && tmpBody.Tables.length > 0 ? tmpBody.Tables : null;
|
|
684
|
+
|
|
685
|
+
let tmpModelTables = tmpCloneState.DeployedModelObject.Tables || {};
|
|
686
|
+
let tmpTableNames = tmpFilterTables || Object.keys(tmpModelTables);
|
|
687
|
+
let tmpCreatedIndices = [];
|
|
688
|
+
|
|
689
|
+
tmpFable.log.info(`Data Cloner: Creating missing GUID indices for ${tmpTableNames.length} tables...`);
|
|
690
|
+
|
|
691
|
+
tmpFable.Utility.eachLimit(tmpTableNames, 1,
|
|
692
|
+
(pTableName, fNextTable) =>
|
|
693
|
+
{
|
|
694
|
+
let tmpTableSchema = tmpModelTables[pTableName];
|
|
695
|
+
if (!tmpTableSchema)
|
|
696
|
+
{
|
|
697
|
+
return fNextTable();
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Generate all index statements for this table (provider-specific)
|
|
701
|
+
let tmpAllStatements = tmpSchemaProvider.generateCreateIndexStatements(tmpTableSchema);
|
|
702
|
+
|
|
703
|
+
// Filter to GUID indices only (AK_M_GUID*)
|
|
704
|
+
let tmpGUIDStatements = tmpAllStatements.filter((pStmt) => pStmt.Name && pStmt.Name.indexOf('AK_M_GUID') === 0);
|
|
705
|
+
|
|
706
|
+
if (tmpGUIDStatements.length < 1)
|
|
707
|
+
{
|
|
708
|
+
return fNextTable();
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// createIndex is idempotent on all providers — safe to call even if index exists
|
|
712
|
+
let fCreateNext = (pIndex) =>
|
|
713
|
+
{
|
|
714
|
+
if (pIndex >= tmpGUIDStatements.length)
|
|
715
|
+
{
|
|
716
|
+
return fNextTable();
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
let tmpStmt = tmpGUIDStatements[pIndex];
|
|
720
|
+
tmpSchemaProvider.createIndex(tmpStmt,
|
|
721
|
+
(pCreateError) =>
|
|
722
|
+
{
|
|
723
|
+
if (pCreateError)
|
|
724
|
+
{
|
|
725
|
+
tmpFable.log.warn(`Data Cloner: Failed to create index ${tmpStmt.Name} on ${pTableName}: ${pCreateError}`);
|
|
726
|
+
}
|
|
727
|
+
else
|
|
728
|
+
{
|
|
729
|
+
tmpFable.log.info(`Data Cloner: Created index ${tmpStmt.Name} on ${pTableName}`);
|
|
730
|
+
tmpCreatedIndices.push(
|
|
731
|
+
{
|
|
732
|
+
Table: pTableName,
|
|
733
|
+
IndexName: tmpStmt.Name,
|
|
734
|
+
Statement: tmpStmt.Statement
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
return fCreateNext(pIndex + 1);
|
|
738
|
+
});
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
fCreateNext(0);
|
|
742
|
+
},
|
|
743
|
+
() =>
|
|
744
|
+
{
|
|
745
|
+
let tmpMessage = tmpCreatedIndices.length > 0
|
|
746
|
+
? `${tmpCreatedIndices.length} GUID index(es) created.`
|
|
747
|
+
: 'No new GUID indices were needed.';
|
|
748
|
+
|
|
749
|
+
tmpFable.log.info(`Data Cloner: ${tmpMessage}`);
|
|
750
|
+
|
|
751
|
+
pResponse.send(200,
|
|
752
|
+
{
|
|
753
|
+
Success: true,
|
|
754
|
+
IndicesCreated: tmpCreatedIndices,
|
|
755
|
+
Message: tmpMessage
|
|
756
|
+
});
|
|
757
|
+
return fNext();
|
|
366
758
|
});
|
|
367
759
|
});
|
|
368
760
|
};
|
|
@@ -164,6 +164,30 @@ class RetoldDataServiceDataCloner extends libFableServiceProviderBase
|
|
|
164
164
|
// and mysql2-native (host, port, user, etc.) property names so it works
|
|
165
165
|
// with any version of the meadow-connection provider.
|
|
166
166
|
let tmpNormalizedConfig = Object.assign({}, pConfig);
|
|
167
|
+
|
|
168
|
+
// Expand a leading ~ to the user's home directory for file-based providers.
|
|
169
|
+
// Node.js does not expand ~ like the shell does, so paths such as
|
|
170
|
+
// "~/my-data/cloned.sqlite" would otherwise be treated as a literal
|
|
171
|
+
// relative path, creating the file in an unexpected location.
|
|
172
|
+
if (tmpNormalizedConfig.SQLiteFilePath && typeof(tmpNormalizedConfig.SQLiteFilePath) === 'string'
|
|
173
|
+
&& tmpNormalizedConfig.SQLiteFilePath.startsWith('~'))
|
|
174
|
+
{
|
|
175
|
+
const libOs = require('os');
|
|
176
|
+
const libPath = require('path');
|
|
177
|
+
const libFs = require('fs');
|
|
178
|
+
tmpNormalizedConfig.SQLiteFilePath = libPath.resolve(
|
|
179
|
+
libOs.homedir(),
|
|
180
|
+
tmpNormalizedConfig.SQLiteFilePath.slice(tmpNormalizedConfig.SQLiteFilePath.startsWith('~/') ? 2 : 1)
|
|
181
|
+
);
|
|
182
|
+
// Ensure the parent directory exists — SQLite will create the file
|
|
183
|
+
// but not intermediate directories.
|
|
184
|
+
let tmpDir = libPath.dirname(tmpNormalizedConfig.SQLiteFilePath);
|
|
185
|
+
if (!libFs.existsSync(tmpDir))
|
|
186
|
+
{
|
|
187
|
+
libFs.mkdirSync(tmpDir, { recursive: true });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
167
191
|
if (pProviderName === 'MySQL' || pProviderName === 'MSSQL' || pProviderName === 'PostgreSQL')
|
|
168
192
|
{
|
|
169
193
|
if (tmpNormalizedConfig.host && !tmpNormalizedConfig.Server)
|
|
@@ -203,9 +227,43 @@ class RetoldDataServiceDataCloner extends libFableServiceProviderBase
|
|
|
203
227
|
}
|
|
204
228
|
else
|
|
205
229
|
{
|
|
230
|
+
let tmpExistingProvider = this.fable[tmpRegistryEntry.serviceName];
|
|
231
|
+
|
|
232
|
+
// Check if the config actually changed — skip reconnect if identical
|
|
233
|
+
let tmpCurrentPath = tmpExistingProvider.options.SQLiteFilePath || '';
|
|
234
|
+
let tmpNewPath = tmpNormalizedConfig.SQLiteFilePath || '';
|
|
235
|
+
let tmpConfigChanged = (pProviderName === 'SQLite')
|
|
236
|
+
? (tmpNewPath !== tmpCurrentPath)
|
|
237
|
+
: (JSON.stringify(tmpNormalizedConfig) !== JSON.stringify(this._cloneState.ConnectionConfig || {}));
|
|
238
|
+
|
|
206
239
|
// Provider already exists — update its options with the new config
|
|
207
240
|
// so reconnects use the current settings.
|
|
208
|
-
|
|
241
|
+
tmpExistingProvider.options[tmpRegistryEntry.configKey] = tmpNormalizedConfig;
|
|
242
|
+
|
|
243
|
+
if (tmpConfigChanged)
|
|
244
|
+
{
|
|
245
|
+
// For SQLite, connectAsync reads SQLiteFilePath directly from
|
|
246
|
+
// this.options (set once in the constructor from fable.settings).
|
|
247
|
+
// Merge the normalized config onto options so the provider sees
|
|
248
|
+
// the updated path without needing a new instance.
|
|
249
|
+
if (pProviderName === 'SQLite')
|
|
250
|
+
{
|
|
251
|
+
Object.assign(tmpExistingProvider.options, tmpNormalizedConfig);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Allow re-connection with updated settings by resetting the
|
|
255
|
+
// connected flag — connectAsync short-circuits when already connected.
|
|
256
|
+
tmpExistingProvider.connected = false;
|
|
257
|
+
}
|
|
258
|
+
else if (tmpExistingProvider.connected)
|
|
259
|
+
{
|
|
260
|
+
// Config unchanged and already connected — skip the connectAsync call
|
|
261
|
+
// entirely to avoid log spam from repeated auto-connect calls.
|
|
262
|
+
this._cloneState.ConnectionProvider = pProviderName;
|
|
263
|
+
this._cloneState.ConnectionConnected = true;
|
|
264
|
+
this._cloneState.ConnectionConfig = pConfig;
|
|
265
|
+
return fCallback();
|
|
266
|
+
}
|
|
209
267
|
}
|
|
210
268
|
|
|
211
269
|
this.fable[tmpRegistryEntry.serviceName].connectAsync(
|