strapi-content-sync-pro 1.0.1 → 1.0.3
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 +84 -25
- package/admin/src/components/ConfigTab.jsx +29 -6
- package/admin/src/components/HelpTab.jsx +131 -32
- 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 +51 -7
- package/admin/src/pages/App/index.jsx +3 -0
- package/docs/Screenshot 2026-04-20 160506.png +0 -0
- package/docs/Screenshot 2026-04-20 160558.png +0 -0
- package/docs/Screenshot 2026-04-20 175903.png +0 -0
- package/docs/Screenshot 2026-04-20 175931.png +0 -0
- package/docs/Screenshot 2026-04-20 180001.png +0 -0
- package/docs/Screenshot 2026-04-20 180041.png +0 -0
- package/docs/Screenshot 2026-04-20 180116.png +0 -0
- package/docs/Screenshot 2026-04-20 180135.png +0 -0
- package/docs/Screenshot 2026-04-20 180202.png +0 -0
- package/docs/Screenshot 2026-04-20 180228.png +0 -0
- package/docs/Screenshot 2026-04-20 180251.png +0 -0
- package/docs/Screenshot 2026-04-20 180301.png +0 -0
- package/docs/clipchamp-screen-recording-script.md +0 -0
- package/docs/logo-horizontal.svg +33 -0
- package/docs/logo-mark.svg +38 -0
- package/docs/logo-square.svg +27 -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 +2 -1
- 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/config.js +48 -5
- package/server/src/controllers/index.js +2 -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 +13 -0
- package/server/src/services/config.js +18 -2
- package/server/src/services/index.js +2 -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 +324 -97
- 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,7 @@ 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');
|
|
15
16
|
|
|
16
17
|
module.exports = {
|
|
17
18
|
ping,
|
|
@@ -26,4 +27,5 @@ module.exports = {
|
|
|
26
27
|
syncMedia,
|
|
27
28
|
alerts,
|
|
28
29
|
dependencies,
|
|
30
|
+
syncStats,
|
|
29
31
|
};
|
|
@@ -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: [] } },
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const STORE_KEY = 'remote-server-config';
|
|
4
4
|
|
|
5
5
|
const SENSITIVE_FIELDS = ['apiToken', 'sharedSecret'];
|
|
6
|
+
const VALID_SYNC_MODES = ['paired', 'single_side'];
|
|
6
7
|
|
|
7
8
|
module.exports = ({ strapi }) => {
|
|
8
9
|
function getStore() {
|
|
@@ -21,11 +22,16 @@ module.exports = ({ strapi }) => {
|
|
|
21
22
|
return null;
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
const normalized = {
|
|
26
|
+
syncMode: 'paired',
|
|
27
|
+
...data,
|
|
28
|
+
};
|
|
29
|
+
|
|
24
30
|
if (!safe) {
|
|
25
|
-
return
|
|
31
|
+
return normalized;
|
|
26
32
|
}
|
|
27
33
|
|
|
28
|
-
const sanitized = { ...
|
|
34
|
+
const sanitized = { ...normalized };
|
|
29
35
|
for (const field of SENSITIVE_FIELDS) {
|
|
30
36
|
if (sanitized[field]) {
|
|
31
37
|
sanitized[field] = '••••••••';
|
|
@@ -59,6 +65,16 @@ module.exports = ({ strapi }) => {
|
|
|
59
65
|
if (config.sharedSecret !== undefined) {
|
|
60
66
|
merged.sharedSecret = config.sharedSecret;
|
|
61
67
|
}
|
|
68
|
+
if (config.syncMode !== undefined) {
|
|
69
|
+
if (!VALID_SYNC_MODES.includes(config.syncMode)) {
|
|
70
|
+
throw new Error(`syncMode must be one of: ${VALID_SYNC_MODES.join(', ')}`);
|
|
71
|
+
}
|
|
72
|
+
merged.syncMode = config.syncMode;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!merged.syncMode) {
|
|
76
|
+
merged.syncMode = 'paired';
|
|
77
|
+
}
|
|
62
78
|
|
|
63
79
|
await store.set({ key: STORE_KEY, value: merged });
|
|
64
80
|
|
|
@@ -12,6 +12,7 @@ const dependencyResolver = require('./dependency-resolver');
|
|
|
12
12
|
const syncEnforcement = require('./sync-enforcement');
|
|
13
13
|
const syncMedia = require('./sync-media');
|
|
14
14
|
const alerts = require('./alerts');
|
|
15
|
+
const syncStats = require('./sync-stats');
|
|
15
16
|
|
|
16
17
|
module.exports = {
|
|
17
18
|
ping,
|
|
@@ -26,5 +27,6 @@ module.exports = {
|
|
|
26
27
|
syncEnforcement,
|
|
27
28
|
syncMedia,
|
|
28
29
|
alerts,
|
|
30
|
+
syncStats,
|
|
29
31
|
};
|
|
30
32
|
|
|
@@ -67,6 +67,8 @@ module.exports = ({ strapi }) => {
|
|
|
67
67
|
retryOnFailure: true,
|
|
68
68
|
retryAttempts: 3,
|
|
69
69
|
retryDelayMs: 5000,
|
|
70
|
+
maxLogEntries: 2000,
|
|
71
|
+
maxReportEntries: 200,
|
|
70
72
|
// Pagination for content-type sync fetches (local + remote). Larger pages
|
|
71
73
|
// are faster but use more memory per chunk. 100 is a safe default.
|
|
72
74
|
syncPageSize: 100,
|
|
@@ -75,6 +77,12 @@ module.exports = ({ strapi }) => {
|
|
|
75
77
|
const VALID_EXECUTION_MODES = ['on_demand', 'scheduled', 'live'];
|
|
76
78
|
const VALID_SCHEDULE_TYPES = ['interval', 'timeout', 'cron', 'external'];
|
|
77
79
|
|
|
80
|
+
async function getSyncMode() {
|
|
81
|
+
const configService = plugin().service('config');
|
|
82
|
+
const config = await configService.getConfig({ safe: false });
|
|
83
|
+
return config?.syncMode || 'paired';
|
|
84
|
+
}
|
|
85
|
+
|
|
78
86
|
// Basic cron validator: 5 or 6 space-separated fields
|
|
79
87
|
function isValidCronExpression(expr) {
|
|
80
88
|
if (typeof expr !== 'string') return false;
|
|
@@ -156,6 +164,11 @@ module.exports = ({ strapi }) => {
|
|
|
156
164
|
throw new Error(`Invalid schedule type "${executionSettings.scheduleType}". Must be one of: ${VALID_SCHEDULE_TYPES.join(', ')}`);
|
|
157
165
|
}
|
|
158
166
|
|
|
167
|
+
const syncMode = await getSyncMode();
|
|
168
|
+
if (syncMode === 'single_side' && executionSettings.executionMode === 'live') {
|
|
169
|
+
throw new Error('Live execution mode is not available in single-side mode. Use on_demand or scheduled.');
|
|
170
|
+
}
|
|
171
|
+
|
|
159
172
|
// Validate schedule interval (used by interval & timeout types)
|
|
160
173
|
if (executionSettings.scheduleInterval !== undefined) {
|
|
161
174
|
if (executionSettings.scheduleInterval < 1 || executionSettings.scheduleInterval > 1440) {
|
|
@@ -185,6 +198,11 @@ module.exports = ({ strapi }) => {
|
|
|
185
198
|
...executionSettings,
|
|
186
199
|
updatedAt: new Date().toISOString(),
|
|
187
200
|
};
|
|
201
|
+
|
|
202
|
+
if (syncMode === 'single_side' && merged.executionMode === 'live') {
|
|
203
|
+
merged.executionMode = 'on_demand';
|
|
204
|
+
merged.enabled = false;
|
|
205
|
+
}
|
|
188
206
|
settings.profiles[profileId] = merged;
|
|
189
207
|
|
|
190
208
|
// Calculate an advisory nextExecutionAt for scheduled mode
|
|
@@ -223,11 +241,20 @@ module.exports = ({ strapi }) => {
|
|
|
223
241
|
const store = getStore();
|
|
224
242
|
const settings = await this.getExecutionSettings();
|
|
225
243
|
|
|
226
|
-
|
|
244
|
+
const next = {
|
|
227
245
|
...settings.globalSettings,
|
|
228
246
|
...globalSettings,
|
|
229
247
|
};
|
|
230
248
|
|
|
249
|
+
if (next.maxLogEntries !== undefined && Number(next.maxLogEntries) < 100) {
|
|
250
|
+
throw new Error('maxLogEntries must be at least 100');
|
|
251
|
+
}
|
|
252
|
+
if (next.maxReportEntries !== undefined && Number(next.maxReportEntries) < 10) {
|
|
253
|
+
throw new Error('maxReportEntries must be at least 10');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
settings.globalSettings = next;
|
|
257
|
+
|
|
231
258
|
await store.set({ key: STORE_KEY, value: settings });
|
|
232
259
|
return settings.globalSettings;
|
|
233
260
|
},
|
|
@@ -238,8 +265,10 @@ module.exports = ({ strapi }) => {
|
|
|
238
265
|
async executeProfile(profileId, options = {}) {
|
|
239
266
|
const syncService = plugin().service('sync');
|
|
240
267
|
const profilesService = plugin().service('syncProfiles');
|
|
268
|
+
const dependencyResolver = plugin().service('dependencyResolver');
|
|
241
269
|
const alertsService = plugin().service('alerts');
|
|
242
270
|
const logService = plugin().service('syncLog');
|
|
271
|
+
const syncStatsService = plugin().service('syncStats');
|
|
243
272
|
|
|
244
273
|
const profile = await profilesService.getProfile(profileId);
|
|
245
274
|
if (!profile) {
|
|
@@ -251,6 +280,11 @@ module.exports = ({ strapi }) => {
|
|
|
251
280
|
const dependencyDepth = options.dependencyDepth ?? executionSettings.dependencyDepth ?? 1;
|
|
252
281
|
|
|
253
282
|
const startTime = new Date();
|
|
283
|
+
const reportHandle = await syncStatsService.createRunReport({
|
|
284
|
+
runType: 'profile_execution',
|
|
285
|
+
trigger: executionSettings.executionMode || 'on_demand',
|
|
286
|
+
contentTypes: [profile.contentType],
|
|
287
|
+
});
|
|
254
288
|
|
|
255
289
|
try {
|
|
256
290
|
// Log execution start
|
|
@@ -263,6 +297,41 @@ module.exports = ({ strapi }) => {
|
|
|
263
297
|
details: { profileId, syncDependencies, dependencyDepth },
|
|
264
298
|
});
|
|
265
299
|
|
|
300
|
+
const dependencyResults = [];
|
|
301
|
+
if (syncDependencies) {
|
|
302
|
+
const dependencyOrder = dependencyResolver
|
|
303
|
+
.getSyncOrder(profile.contentType, dependencyDepth)
|
|
304
|
+
.filter((uid) => uid !== profile.contentType && uid.startsWith('api::') && !!strapi.contentTypes[uid]);
|
|
305
|
+
|
|
306
|
+
for (const dependencyUid of dependencyOrder) {
|
|
307
|
+
const syncConfigService = plugin().service('syncConfig');
|
|
308
|
+
const syncConfig = await syncConfigService.getSyncConfig();
|
|
309
|
+
const depEnabled = (syncConfig.contentTypes || []).some((ct) => ct.uid === dependencyUid && ct.enabled);
|
|
310
|
+
if (!depEnabled) {
|
|
311
|
+
dependencyResults.push({
|
|
312
|
+
uid: dependencyUid,
|
|
313
|
+
profile: null,
|
|
314
|
+
skipped: true,
|
|
315
|
+
reason: 'dependency content-type not enabled for sync',
|
|
316
|
+
});
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const dependencyProfile = await profilesService.getActiveProfileForContentType(dependencyUid);
|
|
321
|
+
const dependencyResult = await syncService.syncContentType(dependencyUid, {
|
|
322
|
+
profile: dependencyProfile,
|
|
323
|
+
syncDependencies: false,
|
|
324
|
+
dependencyDepth: 1,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
dependencyResults.push({
|
|
328
|
+
uid: dependencyUid,
|
|
329
|
+
profile: dependencyProfile ? { id: dependencyProfile.id, name: dependencyProfile.name } : null,
|
|
330
|
+
result: dependencyResult,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
266
335
|
// Execute sync
|
|
267
336
|
const result = await syncService.syncContentType(profile.contentType, {
|
|
268
337
|
profile,
|
|
@@ -270,19 +339,39 @@ module.exports = ({ strapi }) => {
|
|
|
270
339
|
dependencyDepth,
|
|
271
340
|
});
|
|
272
341
|
|
|
342
|
+
const fullResult = {
|
|
343
|
+
...result,
|
|
344
|
+
dependencyResults,
|
|
345
|
+
};
|
|
346
|
+
|
|
273
347
|
// Update last execution time
|
|
274
348
|
await this.updateLastExecution(profileId);
|
|
275
349
|
|
|
350
|
+
await syncStatsService.completeRunReport(reportHandle.reportId, {
|
|
351
|
+
status: 'success',
|
|
352
|
+
summary: fullResult,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const globalSettings = await this.getGlobalSettings();
|
|
356
|
+
await logService.applyRetention({ maxLogs: globalSettings.maxLogEntries });
|
|
357
|
+
await syncStatsService.applyRetention({ maxReports: globalSettings.maxReportEntries });
|
|
358
|
+
|
|
276
359
|
// Send success alert
|
|
277
360
|
await alertsService.sendAlert('sync_success', {
|
|
278
361
|
profile: profile.name,
|
|
279
362
|
contentType: profile.contentType,
|
|
280
|
-
result,
|
|
363
|
+
result: fullResult,
|
|
281
364
|
duration: Date.now() - startTime.getTime(),
|
|
282
365
|
});
|
|
283
366
|
|
|
284
|
-
return
|
|
367
|
+
return fullResult;
|
|
285
368
|
} catch (error) {
|
|
369
|
+
await syncStatsService.completeRunReport(reportHandle.reportId, {
|
|
370
|
+
status: 'error',
|
|
371
|
+
summary: null,
|
|
372
|
+
error: error.message,
|
|
373
|
+
});
|
|
374
|
+
|
|
286
375
|
// Send failure alert
|
|
287
376
|
await alertsService.sendAlert('sync_failure', {
|
|
288
377
|
profile: profile.name,
|
|
@@ -515,23 +604,31 @@ module.exports = ({ strapi }) => {
|
|
|
515
604
|
const profilesService = plugin().service('syncProfiles');
|
|
516
605
|
const profiles = await profilesService.getProfiles();
|
|
517
606
|
|
|
607
|
+
const syncMode = await getSyncMode();
|
|
518
608
|
const status = [];
|
|
519
609
|
for (const profile of profiles) {
|
|
520
610
|
const execSettings = settings.profiles[profile.id] || {};
|
|
521
611
|
const handle = schedulerHandles[profile.id];
|
|
612
|
+
const executionMode = syncMode === 'single_side' && execSettings.executionMode === 'live'
|
|
613
|
+
? 'on_demand'
|
|
614
|
+
: (execSettings.executionMode || 'on_demand');
|
|
615
|
+
|
|
522
616
|
status.push({
|
|
523
617
|
profileId: profile.id,
|
|
524
618
|
profileName: profile.name,
|
|
525
619
|
contentType: profile.contentType,
|
|
526
|
-
executionMode
|
|
620
|
+
executionMode,
|
|
527
621
|
scheduleType: execSettings.scheduleType || null,
|
|
528
622
|
scheduleInterval: execSettings.scheduleInterval || null,
|
|
529
623
|
cronExpression: execSettings.cronExpression || null,
|
|
530
|
-
enabled: execSettings.
|
|
624
|
+
enabled: syncMode === 'single_side' && execSettings.executionMode === 'live'
|
|
625
|
+
? false
|
|
626
|
+
: (execSettings.enabled || false),
|
|
531
627
|
lastExecutedAt: execSettings.lastExecutedAt || null,
|
|
532
628
|
nextExecutionAt: execSettings.nextExecutionAt || null,
|
|
533
629
|
isSchedulerRunning: !!handle && !handle.external,
|
|
534
630
|
isExternal: !!(handle && handle.external),
|
|
631
|
+
syncMode,
|
|
535
632
|
});
|
|
536
633
|
}
|
|
537
634
|
|
|
@@ -53,4 +53,40 @@ module.exports = ({ strapi }) => ({
|
|
|
53
53
|
},
|
|
54
54
|
};
|
|
55
55
|
},
|
|
56
|
+
|
|
57
|
+
async clearLogs() {
|
|
58
|
+
const existing = await strapi.documents(CONTENT_TYPE_UID).findMany({
|
|
59
|
+
fields: ['documentId'],
|
|
60
|
+
limit: 10000,
|
|
61
|
+
sort: { createdAt: 'desc' },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
for (const entry of existing) {
|
|
65
|
+
if (!entry?.documentId) continue;
|
|
66
|
+
await strapi.documents(CONTENT_TYPE_UID).delete({ documentId: entry.documentId });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { deleted: existing.length };
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
async applyRetention({ maxLogs = 2000 } = {}) {
|
|
73
|
+
const safeMax = Math.max(100, Number(maxLogs) || 2000);
|
|
74
|
+
const count = await strapi.documents(CONTENT_TYPE_UID).count();
|
|
75
|
+
if (count <= safeMax) return { pruned: 0, remaining: count };
|
|
76
|
+
|
|
77
|
+
const excess = count - safeMax;
|
|
78
|
+
const toDelete = await strapi.documents(CONTENT_TYPE_UID).findMany({
|
|
79
|
+
fields: ['documentId'],
|
|
80
|
+
sort: { createdAt: 'asc' },
|
|
81
|
+
limit: excess,
|
|
82
|
+
start: 0,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
for (const entry of toDelete) {
|
|
86
|
+
if (!entry?.documentId) continue;
|
|
87
|
+
await strapi.documents(CONTENT_TYPE_UID).delete({ documentId: entry.documentId });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { pruned: toDelete.length, remaining: count - toDelete.length };
|
|
91
|
+
},
|
|
56
92
|
});
|