strapi-content-sync-pro 1.0.2 → 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.
Files changed (36) hide show
  1. package/README.md +67 -18
  2. package/admin/src/components/BulkTransferTab.jsx +880 -0
  3. package/admin/src/components/ConfigTab.jsx +25 -4
  4. package/admin/src/components/HelpTab.jsx +201 -15
  5. package/admin/src/components/MediaTab.jsx +7 -0
  6. package/admin/src/components/StatsTab.jsx +470 -0
  7. package/admin/src/components/SyncProfilesTab.jsx +63 -5
  8. package/admin/src/components/SyncTab.jsx +53 -7
  9. package/admin/src/pages/App/index.jsx +15 -1
  10. package/docs/clipchamp-screen-recording-script.md +0 -0
  11. package/docs/production-readiness-status.md +34 -0
  12. package/docs/production-readiness-test-matrix.md +151 -0
  13. package/docs/test-environments-setup-legacy.txt +60 -0
  14. package/package.json +13 -4
  15. package/server/src/content-types/index.js +2 -0
  16. package/server/src/content-types/sync-run-report/schema.json +26 -0
  17. package/server/src/controllers/bulk-transfer.js +141 -0
  18. package/server/src/controllers/config.js +48 -5
  19. package/server/src/controllers/index.js +4 -0
  20. package/server/src/controllers/sync-log.js +6 -0
  21. package/server/src/controllers/sync-media.js +19 -0
  22. package/server/src/controllers/sync-stats.js +51 -0
  23. package/server/src/controllers/sync.js +9 -3
  24. package/server/src/routes/index.js +28 -0
  25. package/server/src/services/bulk-transfer.js +837 -0
  26. package/server/src/services/config.js +18 -2
  27. package/server/src/services/index.js +4 -0
  28. package/server/src/services/sync-execution.js +102 -5
  29. package/server/src/services/sync-log.js +36 -0
  30. package/server/src/services/sync-media.js +224 -1
  31. package/server/src/services/sync-profiles.js +92 -4
  32. package/server/src/services/sync-stats.js +353 -0
  33. package/server/src/services/sync.js +323 -101
  34. package/server/src/utils/applier.js +120 -13
  35. package/server/src/utils/comparator.js +22 -6
  36. package/server/src/utils/fetcher.js +11 -2
@@ -38,8 +38,50 @@ module.exports = {
38
38
  };
39
39
  }
40
40
 
41
- // Step 1: Basic reachability via public ping endpoint
41
+ const syncMode = config.syncMode || 'paired';
42
+
43
+ // Step 1: Basic reachability
42
44
  const startTime = Date.now();
45
+ if (syncMode === 'single_side') {
46
+ try {
47
+ const reachRes = await fetch(`${config.baseUrl}/api`, {
48
+ method: 'GET',
49
+ headers: { Authorization: `Bearer ${config.apiToken}` },
50
+ });
51
+
52
+ if (reachRes.status === 401 || reachRes.status === 403) {
53
+ return ctx.body = {
54
+ data: {
55
+ success: false,
56
+ stage: 'auth',
57
+ message: `API token rejected by remote server (${reachRes.status}). Verify the token is valid and can read target content APIs.`,
58
+ latency: Date.now() - startTime,
59
+ },
60
+ };
61
+ }
62
+ } catch (err) {
63
+ return ctx.body = {
64
+ data: {
65
+ success: false,
66
+ stage: 'network',
67
+ message: `Cannot reach remote server in single-side mode: ${err.message}`,
68
+ latency: Date.now() - startTime,
69
+ },
70
+ };
71
+ }
72
+
73
+ return ctx.body = {
74
+ data: {
75
+ success: true,
76
+ stage: 'complete',
77
+ message: 'Connection successful in single-side mode. Remote plugin endpoints are not required; only remote content APIs and token access are validated.',
78
+ latency: Date.now() - startTime,
79
+ remoteInfo: null,
80
+ mode: syncMode,
81
+ },
82
+ };
83
+ }
84
+
43
85
  let pingStatus = null;
44
86
  try {
45
87
  const pingRes = await fetch(`${config.baseUrl}/api/${PLUGIN_ID}/ping`, {
@@ -69,7 +111,7 @@ module.exports = {
69
111
 
70
112
  const pingLatency = Date.now() - startTime;
71
113
 
72
- // Step 2: Verify API token works against an authenticated endpoint
114
+ // Step 2: Verify API token works against an authenticated plugin endpoint
73
115
  let authWorks = false;
74
116
  let remoteInfo = null;
75
117
  try {
@@ -116,10 +158,11 @@ module.exports = {
116
158
  success: true,
117
159
  stage: 'complete',
118
160
  message: authWorks
119
- ? 'Connection successful and API token validated'
161
+ ? 'Connection successful: remote plugin is reachable and API token is valid. Ensure matching sync settings (content types, active profiles, execution mode, and shared secret) on both servers before running sync.'
120
162
  : 'Reachable but API token could not be validated',
121
163
  latency: pingLatency,
122
164
  remoteInfo,
165
+ mode: syncMode,
123
166
  },
124
167
  };
125
168
  },
@@ -251,10 +294,10 @@ module.exports = {
251
294
  // Step 4: Optionally get remote instance ID if plugin is installed
252
295
  let remoteInstanceId = null;
253
296
  try {
254
- const remoteConfigResponse = await fetch(`${baseUrl}/api/${PLUGIN_ID}/config`, {
297
+ const remoteConfigResponse = await fetch(`${baseUrl}/strapi-content-sync-pro/config`, {
255
298
  method: 'GET',
256
299
  headers: {
257
- Authorization: `Bearer ${apiToken}`,
300
+ Authorization: `Bearer ${adminJwt}`,
258
301
  },
259
302
  });
260
303
 
@@ -12,6 +12,8 @@ const syncEnforcement = require('./sync-enforcement');
12
12
  const syncMedia = require('./sync-media');
13
13
  const alerts = require('./alerts');
14
14
  const dependencies = require('./dependencies');
15
+ const syncStats = require('./sync-stats');
16
+ const bulkTransfer = require('./bulk-transfer');
15
17
 
16
18
  module.exports = {
17
19
  ping,
@@ -26,4 +28,6 @@ module.exports = {
26
28
  syncMedia,
27
29
  alerts,
28
30
  dependencies,
31
+ syncStats,
32
+ bulkTransfer,
29
33
  };
@@ -15,4 +15,10 @@ module.exports = {
15
15
 
16
16
  ctx.body = result;
17
17
  },
18
+
19
+ async clear(ctx) {
20
+ const service = strapi.plugin('strapi-content-sync-pro').service('syncLog');
21
+ const result = await service.clearLogs();
22
+ ctx.body = { data: result };
23
+ },
18
24
  };
@@ -111,6 +111,25 @@ module.exports = ({ strapi }) => ({
111
111
  }
112
112
  },
113
113
 
114
+ // ── Morph link sync (documentId-based mapping) ───────────────────────────
115
+
116
+ async getMorphLinks(ctx) {
117
+ try {
118
+ ctx.body = { data: await service(strapi).exportMorphLinks() };
119
+ } catch (err) {
120
+ ctx.throw(500, err.message);
121
+ }
122
+ },
123
+
124
+ async applyMorphLinks(ctx) {
125
+ try {
126
+ const links = ctx.request.body?.links || [];
127
+ ctx.body = { data: await service(strapi).applyMorphLinks(links) };
128
+ } catch (err) {
129
+ ctx.throw(400, err.message);
130
+ }
131
+ },
132
+
114
133
  // ── Back-compat (old flat endpoints) ──────────────────────────────────────
115
134
 
116
135
  async getSettings(ctx) {
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ const PLUGIN_ID = 'strapi-content-sync-pro';
4
+
5
+ module.exports = ({ strapi }) => ({
6
+ async getSnapshot(ctx) {
7
+ try {
8
+ const data = await strapi.plugin(PLUGIN_ID).service('syncStats').getLatestSnapshot();
9
+ ctx.body = { data };
10
+ } catch (err) {
11
+ ctx.throw(500, err.message);
12
+ }
13
+ },
14
+
15
+ async getReports(ctx) {
16
+ const { page, pageSize } = ctx.query;
17
+ try {
18
+ const data = await strapi.plugin(PLUGIN_ID).service('syncStats').getReports({
19
+ page: page ? parseInt(page, 10) : 1,
20
+ pageSize: pageSize ? parseInt(pageSize, 10) : 10,
21
+ });
22
+ ctx.body = data;
23
+ } catch (err) {
24
+ ctx.throw(500, err.message);
25
+ }
26
+ },
27
+
28
+ async clearReports(ctx) {
29
+ try {
30
+ const result = await strapi.plugin(PLUGIN_ID).service('syncStats').clearReports();
31
+ ctx.body = { data: result };
32
+ } catch (err) {
33
+ ctx.throw(500, err.message);
34
+ }
35
+ },
36
+
37
+ async runRetention(ctx) {
38
+ const body = ctx.request.body || {};
39
+ try {
40
+ const syncStats = strapi.plugin(PLUGIN_ID).service('syncStats');
41
+ const syncLog = strapi.plugin(PLUGIN_ID).service('syncLog');
42
+ const reports = await syncStats.applyRetention({ maxReports: body.maxReports });
43
+ const logs = await syncLog.applyRetention({ maxLogs: body.maxLogs });
44
+ ctx.body = { data: { reports, logs } };
45
+ } catch (err) {
46
+ ctx.throw(400, err.message);
47
+ }
48
+ },
49
+
50
+
51
+ });
@@ -15,14 +15,20 @@ module.exports = {
15
15
  async receive(ctx) {
16
16
  const { body } = ctx.request;
17
17
 
18
- if (!body || !body.uid || !body.syncId) {
19
- return ctx.badRequest('Missing uid, data, or syncId');
18
+ if (!body || !body.uid || (!body.syncId && !body.documentId)) {
19
+ return ctx.badRequest('Missing uid, data, or documentId/syncId');
20
20
  }
21
21
 
22
22
  const syncService = strapi.plugin('strapi-content-sync-pro').service('sync');
23
23
 
24
24
  try {
25
- const result = await syncService.receiveRecord(body.uid, body.data || {}, body.syncId);
25
+ const result = await syncService.receiveRecord(
26
+ body.uid,
27
+ body.data || {},
28
+ body.syncId || null,
29
+ !!body.delete,
30
+ body.documentId || null,
31
+ );
26
32
  ctx.body = { data: result };
27
33
  } catch (err) {
28
34
  return ctx.badRequest(err.message);
@@ -13,6 +13,10 @@ const contentApiRoutes = [
13
13
  { method: 'GET', path: '/enforcement/local-info', handler: 'syncEnforcement.getLocalInfo', config: { policies: [] } },
14
14
  { method: 'GET', path: '/enforcement/schema/:uid', handler: 'syncEnforcement.getLocalSchema', config: { policies: [] } },
15
15
  { method: 'POST', path: '/receive', handler: 'sync.receive', config: { policies: [], auth: false, middlewares: [verifySignature] } },
16
+
17
+ // Media morph-link sync (called by the peer instance during runProfile)
18
+ { method: 'GET', path: '/media-sync/morph-links', handler: 'syncMedia.getMorphLinks', config: { policies: [] } },
19
+ { method: 'POST', path: '/media-sync/morph-links/apply', handler: 'syncMedia.applyMorphLinks', config: { policies: [] } },
16
20
  ];
17
21
 
18
22
  const adminRoutes = [
@@ -35,6 +39,13 @@ const adminRoutes = [
35
39
  // Logs
36
40
  { method: 'GET', path: '/logs', handler: 'syncLog.find', config: { policies: [] } },
37
41
 
42
+ // Stats and retention
43
+ { method: 'GET', path: '/stats/snapshot', handler: 'syncStats.getSnapshot', config: { policies: [] } },
44
+ { method: 'GET', path: '/stats/reports', handler: 'syncStats.getReports', config: { policies: [] } },
45
+ { method: 'POST', path: '/stats/reports/clear', handler: 'syncStats.clearReports', config: { policies: [] } },
46
+ { method: 'POST', path: '/stats/retention/run', handler: 'syncStats.runRetention', config: { policies: [] } },
47
+ { method: 'POST', path: '/logs/clear', handler: 'syncLog.clear', config: { policies: [] } },
48
+
38
49
  // Sync Profiles
39
50
  { method: 'GET', path: '/sync-profiles', handler: 'syncProfiles.find', config: { policies: [] } },
40
51
  { method: 'GET', path: '/sync-profiles/:id', handler: 'syncProfiles.findOne', config: { policies: [] } },
@@ -81,6 +92,8 @@ const adminRoutes = [
81
92
  { method: 'POST', path: '/media-sync/profiles/:id/activate', handler: 'syncMedia.activateProfile', config: { policies: [] } },
82
93
  { method: 'POST', path: '/media-sync/profiles/:id/run', handler: 'syncMedia.runProfile', config: { policies: [] } },
83
94
  { method: 'POST', path: '/media-sync/run-active', handler: 'syncMedia.runActiveProfiles', config: { policies: [] } },
95
+ { method: 'GET', path: '/media-sync/morph-links', handler: 'syncMedia.getMorphLinks', config: { policies: [] } },
96
+ { method: 'POST', path: '/media-sync/morph-links/apply', handler: 'syncMedia.applyMorphLinks', config: { policies: [] } },
84
97
  { method: 'GET', path: '/media-sync/global-settings', handler: 'syncMedia.getGlobalSettings', config: { policies: [] } },
85
98
  { method: 'PUT', path: '/media-sync/global-settings', handler: 'syncMedia.updateGlobalSettings', config: { policies: [] } },
86
99
  { method: 'GET', path: '/media-sync/defaults', handler: 'syncMedia.getDefaults', config: { policies: [] } },
@@ -97,6 +110,21 @@ const adminRoutes = [
97
110
  { method: 'GET', path: '/dependencies/:uid/sync-order', handler: 'dependencies.getSyncOrder', config: { policies: [] } },
98
111
  { method: 'GET', path: '/dependencies/:uid/summary', handler: 'dependencies.getSummary', config: { policies: [] } },
99
112
  { method: 'POST', path: '/dependencies/clear-cache', handler: 'dependencies.clearCache', config: { policies: [] } },
113
+
114
+ // Bulk Transfer (one-click full pull / full push)
115
+ { method: 'POST', path: '/bulk-transfer/preview', handler: 'bulkTransfer.preview', config: { policies: [] } },
116
+ { method: 'POST', path: '/bulk-transfer/start', handler: 'bulkTransfer.start', config: { policies: [] } },
117
+ { method: 'GET', path: '/bulk-transfer/jobs', handler: 'bulkTransfer.list', config: { policies: [] } },
118
+ { method: 'GET', path: '/bulk-transfer/jobs/:jobId', handler: 'bulkTransfer.status', config: { policies: [] } },
119
+ { method: 'POST', path: '/bulk-transfer/jobs/:jobId/next', handler: 'bulkTransfer.next', config: { policies: [] } },
120
+ { method: 'POST', path: '/bulk-transfer/jobs/:jobId/run-all', handler: 'bulkTransfer.runAll', config: { policies: [] } },
121
+ { method: 'POST', path: '/bulk-transfer/jobs/:jobId/pause', handler: 'bulkTransfer.pause', config: { policies: [] } },
122
+ { method: 'POST', path: '/bulk-transfer/jobs/:jobId/resume', handler: 'bulkTransfer.resume', config: { policies: [] } },
123
+ { method: 'POST', path: '/bulk-transfer/jobs/:jobId/cancel', handler: 'bulkTransfer.cancel', config: { policies: [] } },
124
+ { method: 'GET', path: '/bulk-transfer/history', handler: 'bulkTransfer.history', config: { policies: [] } },
125
+ { method: 'POST', path: '/bulk-transfer/history/clear', handler: 'bulkTransfer.clearHistory', config: { policies: [] } },
126
+ { method: 'POST', path: '/bulk-transfer/history/:historyId/restart', handler: 'bulkTransfer.restart', config: { policies: [] } },
127
+ { method: 'POST', path: '/bulk-transfer/history/:historyId/resume', handler: 'bulkTransfer.resumeFromHistory', config: { policies: [] } },
100
128
  ];
101
129
 
102
130
  module.exports = {