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.
- package/README.md +67 -18
- package/admin/src/components/BulkTransferTab.jsx +880 -0
- package/admin/src/components/ConfigTab.jsx +25 -4
- package/admin/src/components/HelpTab.jsx +201 -15
- package/admin/src/components/MediaTab.jsx +7 -0
- package/admin/src/components/StatsTab.jsx +470 -0
- package/admin/src/components/SyncProfilesTab.jsx +63 -5
- package/admin/src/components/SyncTab.jsx +53 -7
- package/admin/src/pages/App/index.jsx +15 -1
- package/docs/clipchamp-screen-recording-script.md +0 -0
- package/docs/production-readiness-status.md +34 -0
- package/docs/production-readiness-test-matrix.md +151 -0
- package/docs/test-environments-setup-legacy.txt +60 -0
- package/package.json +13 -4
- package/server/src/content-types/index.js +2 -0
- package/server/src/content-types/sync-run-report/schema.json +26 -0
- package/server/src/controllers/bulk-transfer.js +141 -0
- package/server/src/controllers/config.js +48 -5
- package/server/src/controllers/index.js +4 -0
- package/server/src/controllers/sync-log.js +6 -0
- package/server/src/controllers/sync-media.js +19 -0
- package/server/src/controllers/sync-stats.js +51 -0
- package/server/src/controllers/sync.js +9 -3
- package/server/src/routes/index.js +28 -0
- package/server/src/services/bulk-transfer.js +837 -0
- package/server/src/services/config.js +18 -2
- package/server/src/services/index.js +4 -0
- package/server/src/services/sync-execution.js +102 -5
- package/server/src/services/sync-log.js +36 -0
- package/server/src/services/sync-media.js +224 -1
- package/server/src/services/sync-profiles.js +92 -4
- package/server/src/services/sync-stats.js +353 -0
- package/server/src/services/sync.js +323 -101
- package/server/src/utils/applier.js +120 -13
- package/server/src/utils/comparator.js +22 -6
- package/server/src/utils/fetcher.js +11 -2
|
@@ -38,8 +38,50 @@ module.exports = {
|
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
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
|
|
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}/
|
|
297
|
+
const remoteConfigResponse = await fetch(`${baseUrl}/strapi-content-sync-pro/config`, {
|
|
255
298
|
method: 'GET',
|
|
256
299
|
headers: {
|
|
257
|
-
Authorization: `Bearer ${
|
|
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
|
};
|
|
@@ -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(
|
|
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 = {
|