strapi-content-sync-pro 1.0.3 → 1.0.4
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 +2 -0
- package/admin/src/components/BulkTransferTab.jsx +880 -0
- package/admin/src/components/HelpTab.jsx +114 -5
- package/admin/src/components/SyncTab.jsx +2 -0
- package/admin/src/pages/App/index.jsx +12 -1
- package/package.json +13 -4
- package/server/src/controllers/bulk-transfer.js +141 -0
- package/server/src/controllers/index.js +2 -0
- package/server/src/routes/index.js +15 -0
- package/server/src/services/bulk-transfer.js +837 -0
- package/server/src/services/index.js +2 -0
- package/server/src/services/sync.js +137 -1
|
@@ -13,6 +13,7 @@ const syncEnforcement = require('./sync-enforcement');
|
|
|
13
13
|
const syncMedia = require('./sync-media');
|
|
14
14
|
const alerts = require('./alerts');
|
|
15
15
|
const syncStats = require('./sync-stats');
|
|
16
|
+
const bulkTransfer = require('./bulk-transfer');
|
|
16
17
|
|
|
17
18
|
module.exports = {
|
|
18
19
|
ping,
|
|
@@ -28,5 +29,6 @@ module.exports = {
|
|
|
28
29
|
syncMedia,
|
|
29
30
|
alerts,
|
|
30
31
|
syncStats,
|
|
32
|
+
bulkTransfer,
|
|
31
33
|
};
|
|
32
34
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { fetchLocalRecords, fetchRemoteRecords } = require('../utils/fetcher');
|
|
3
|
+
const { fetchLocalRecords, fetchRemoteRecords, fetchLocalPage, fetchRemotePage } = require('../utils/fetcher');
|
|
4
4
|
const { compareRecords } = require('../utils/comparator');
|
|
5
5
|
const { applyLocal, applyRemote, deleteLocal, deleteRemote } = require('../utils/applier');
|
|
6
6
|
|
|
@@ -372,6 +372,142 @@ module.exports = ({ strapi }) => {
|
|
|
372
372
|
}
|
|
373
373
|
},
|
|
374
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Sync a SINGLE PAGE of a content type. Used by the bulk-transfer (Full
|
|
377
|
+
* Sync) engine to process large content types page-by-page so that
|
|
378
|
+
* progress can be reported and the job can be paused / resumed between
|
|
379
|
+
* pages.
|
|
380
|
+
*
|
|
381
|
+
* options:
|
|
382
|
+
* - profile: synthetic/real profile (direction, conflictStrategy, syncDeletions)
|
|
383
|
+
* - page: 1-based page number (default 1)
|
|
384
|
+
* - pageSize: records per page (default from global settings or 100)
|
|
385
|
+
* - lastSyncAt: optional ISO timestamp; when omitted this runs a full
|
|
386
|
+
* page scan (preferred for bulk transfer). When provided it acts
|
|
387
|
+
* incremental.
|
|
388
|
+
*
|
|
389
|
+
* Returns:
|
|
390
|
+
* { uid, page, pageSize, pushed, pulled, errors, hasMore,
|
|
391
|
+
* localCount, remoteCount, remoteTotal, remotePageCount }
|
|
392
|
+
*/
|
|
393
|
+
async syncContentTypePage(uid, options = {}) {
|
|
394
|
+
if (!uid) throw new Error('Content type uid is required');
|
|
395
|
+
|
|
396
|
+
const logService = plugin().service('syncLog');
|
|
397
|
+
const configService = plugin().service('config');
|
|
398
|
+
const syncConfigService = plugin().service('syncConfig');
|
|
399
|
+
const syncProfilesService = plugin().service('syncProfiles');
|
|
400
|
+
const executionService = plugin().service('syncExecution');
|
|
401
|
+
|
|
402
|
+
const remoteConfig = await configService.getConfig({ safe: false });
|
|
403
|
+
if (!remoteConfig || !remoteConfig.baseUrl) {
|
|
404
|
+
throw new Error('Remote server not configured');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const { profile } = options;
|
|
408
|
+
const syncConfig = await syncConfigService.getSyncConfig();
|
|
409
|
+
const ctConfig = (syncConfig.contentTypes || []).find((ct) => ct.uid === uid) || { uid, fields: [] };
|
|
410
|
+
|
|
411
|
+
const direction = profile?.direction || ctConfig.direction || 'both';
|
|
412
|
+
const conflictStrategy = profile?.conflictStrategy || syncConfig.conflictStrategy || 'latest';
|
|
413
|
+
const syncDeletions = !!(profile?.syncDeletions);
|
|
414
|
+
const fields = ctConfig.fields || [];
|
|
415
|
+
|
|
416
|
+
let fieldPolicies = null;
|
|
417
|
+
if (profile && !profile.isSimple && Array.isArray(profile.fieldPolicies) && profile.fieldPolicies.length > 0) {
|
|
418
|
+
fieldPolicies = {};
|
|
419
|
+
for (const fp of profile.fieldPolicies) fieldPolicies[fp.field] = fp.direction;
|
|
420
|
+
} else if (!profile) {
|
|
421
|
+
fieldPolicies = await syncProfilesService.getFieldPoliciesForContentType(uid);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const globalExec = (await executionService.getGlobalSettings?.()) || {};
|
|
425
|
+
const pageSize = Number(options.pageSize) || Number(globalExec.syncPageSize) || 100;
|
|
426
|
+
const page = Math.max(1, Number(options.page) || 1);
|
|
427
|
+
const lastSyncAt = options.lastSyncAt || null;
|
|
428
|
+
|
|
429
|
+
let pushed = 0;
|
|
430
|
+
let pulled = 0;
|
|
431
|
+
let errors = 0;
|
|
432
|
+
|
|
433
|
+
const localPageRes = await fetchLocalPage(strapi, uid, { fields, lastSyncAt, page, pageSize });
|
|
434
|
+
const remotePageRes = await fetchRemotePage(remoteConfig, uid, { fields, lastSyncAt, page, pageSize });
|
|
435
|
+
|
|
436
|
+
const localRecords = localPageRes.records || [];
|
|
437
|
+
const remoteRecords = remotePageRes.records || [];
|
|
438
|
+
|
|
439
|
+
// NOTE: comparator works on the page slice only. Cross-side deletion
|
|
440
|
+
// detection is intentionally disabled here because a record missing
|
|
441
|
+
// from this page may live on another page; full-set deletion sync
|
|
442
|
+
// should use the incremental path instead.
|
|
443
|
+
const diff = compareRecords(localRecords, remoteRecords, {
|
|
444
|
+
direction,
|
|
445
|
+
conflictStrategy,
|
|
446
|
+
syncDeletions: false,
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
for (const { local } of diff.toPush) {
|
|
450
|
+
try {
|
|
451
|
+
const filtered = syncProfilesService.filterFieldsByPolicy(local, fieldPolicies, 'push');
|
|
452
|
+
await applyRemote(remoteConfig, uid, filtered, fields);
|
|
453
|
+
pushed++;
|
|
454
|
+
} catch (err) {
|
|
455
|
+
errors++;
|
|
456
|
+
await logService.log({ action: 'push', contentType: uid, syncId: local.syncId, direction: 'push', status: 'error', message: err.message });
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
for (const { remote } of diff.toPull) {
|
|
461
|
+
try {
|
|
462
|
+
const filtered = syncProfilesService.filterFieldsByPolicy(remote, fieldPolicies, 'pull');
|
|
463
|
+
await applyLocal(strapi, uid, filtered, fields);
|
|
464
|
+
pulled++;
|
|
465
|
+
} catch (err) {
|
|
466
|
+
errors++;
|
|
467
|
+
await logService.log({ action: 'pull', contentType: uid, syncId: remote.syncId, direction: 'pull', status: 'error', message: err.message });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
for (const record of diff.toCreateRemote) {
|
|
472
|
+
try {
|
|
473
|
+
const filtered = syncProfilesService.filterFieldsByPolicy(record, fieldPolicies, 'push');
|
|
474
|
+
await applyRemote(remoteConfig, uid, filtered, fields);
|
|
475
|
+
pushed++;
|
|
476
|
+
} catch (err) {
|
|
477
|
+
errors++;
|
|
478
|
+
await logService.log({ action: 'create_remote', contentType: uid, syncId: record.syncId, direction: 'push', status: 'error', message: err.message });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
for (const record of diff.toCreateLocal) {
|
|
483
|
+
try {
|
|
484
|
+
const filtered = syncProfilesService.filterFieldsByPolicy(record, fieldPolicies, 'pull');
|
|
485
|
+
await applyLocal(strapi, uid, filtered, fields);
|
|
486
|
+
pulled++;
|
|
487
|
+
} catch (err) {
|
|
488
|
+
errors++;
|
|
489
|
+
await logService.log({ action: 'create_local', contentType: uid, syncId: record.syncId, direction: 'pull', status: 'error', message: err.message });
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// hasMore is the OR of both sides so we keep paging until both are drained
|
|
494
|
+
const hasMore = !!(localPageRes.hasMore || remotePageRes.hasMore);
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
uid,
|
|
498
|
+
page,
|
|
499
|
+
pageSize,
|
|
500
|
+
pushed,
|
|
501
|
+
pulled,
|
|
502
|
+
errors,
|
|
503
|
+
hasMore,
|
|
504
|
+
localCount: localRecords.length,
|
|
505
|
+
remoteCount: remoteRecords.length,
|
|
506
|
+
remoteTotal: remotePageRes.total,
|
|
507
|
+
remotePageCount: remotePageRes.pageCount,
|
|
508
|
+
};
|
|
509
|
+
},
|
|
510
|
+
|
|
375
511
|
/**
|
|
376
512
|
* Step 8 — Push a single record to the remote (called by lifecycle hooks).
|
|
377
513
|
* Now supports field-level policies.
|