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.
@@ -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.