strapi-content-sync-pro 1.0.0
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/LICENSE +21 -0
- package/README.md +206 -0
- package/admin/src/components/ConfigTab.jsx +1038 -0
- package/admin/src/components/ContentTypesTab.jsx +160 -0
- package/admin/src/components/HelpTab.jsx +945 -0
- package/admin/src/components/LogsTab.jsx +136 -0
- package/admin/src/components/MediaTab.jsx +557 -0
- package/admin/src/components/SyncProfilesTab.jsx +715 -0
- package/admin/src/components/SyncTab.jsx +988 -0
- package/admin/src/index.js +31 -0
- package/admin/src/pages/App/index.jsx +129 -0
- package/admin/src/pluginId.js +3 -0
- package/package.json +84 -0
- package/server/src/bootstrap.js +151 -0
- package/server/src/config/index.js +5 -0
- package/server/src/content-types/index.js +7 -0
- package/server/src/content-types/sync-log/schema.json +24 -0
- package/server/src/controllers/alerts.js +59 -0
- package/server/src/controllers/config.js +292 -0
- package/server/src/controllers/content-type-discovery.js +9 -0
- package/server/src/controllers/dependencies.js +109 -0
- package/server/src/controllers/index.js +29 -0
- package/server/src/controllers/ping.js +7 -0
- package/server/src/controllers/sync-config.js +26 -0
- package/server/src/controllers/sync-enforcement.js +323 -0
- package/server/src/controllers/sync-execution.js +134 -0
- package/server/src/controllers/sync-log.js +18 -0
- package/server/src/controllers/sync-media.js +158 -0
- package/server/src/controllers/sync-profiles.js +182 -0
- package/server/src/controllers/sync.js +31 -0
- package/server/src/destroy.js +7 -0
- package/server/src/index.js +21 -0
- package/server/src/middlewares/verify-signature.js +32 -0
- package/server/src/register.js +7 -0
- package/server/src/routes/index.js +111 -0
- package/server/src/services/alerts.js +437 -0
- package/server/src/services/config.js +68 -0
- package/server/src/services/content-type-discovery.js +41 -0
- package/server/src/services/dependency-resolver.js +284 -0
- package/server/src/services/index.js +30 -0
- package/server/src/services/ping.js +7 -0
- package/server/src/services/sync-config.js +45 -0
- package/server/src/services/sync-enforcement.js +362 -0
- package/server/src/services/sync-execution.js +541 -0
- package/server/src/services/sync-log.js +56 -0
- package/server/src/services/sync-media.js +963 -0
- package/server/src/services/sync-profiles.js +380 -0
- package/server/src/services/sync.js +248 -0
- package/server/src/utils/applier.js +89 -0
- package/server/src/utils/comparator.js +83 -0
- package/server/src/utils/fetcher.js +142 -0
- package/server/src/utils/hmac.js +37 -0
- package/server/src/utils/pagination.js +51 -0
- package/server/src/utils/sync-guard.js +29 -0
- package/server/src/utils/sync-id.js +16 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const STORE_KEY = 'sync-enforcement-settings';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Sync Enforcement Service
|
|
7
|
+
*
|
|
8
|
+
* Enforces sync compatibility checks before execution:
|
|
9
|
+
* - Schema match: Verify content type schemas are compatible
|
|
10
|
+
* - Version check: Ensure Strapi versions are compatible
|
|
11
|
+
* - DateTime sync: Validate timestamps between instances
|
|
12
|
+
*
|
|
13
|
+
* Settings are stored in plugin configuration.
|
|
14
|
+
*/
|
|
15
|
+
module.exports = ({ strapi }) => {
|
|
16
|
+
function getStore() {
|
|
17
|
+
return strapi.store({ type: 'plugin', name: 'strapi-content-sync-pro' });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function plugin() {
|
|
21
|
+
return strapi.plugin('strapi-content-sync-pro');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const DEFAULT_ENFORCEMENT_SETTINGS = {
|
|
25
|
+
enforceSchemaMatch: true,
|
|
26
|
+
schemaMatchMode: 'strict', // 'strict' | 'compatible' | 'none'
|
|
27
|
+
enforceVersionCheck: true,
|
|
28
|
+
allowedVersionDrift: 'minor', // 'exact' | 'minor' | 'major' | 'none'
|
|
29
|
+
enforceDateTimeSync: true,
|
|
30
|
+
maxTimeDriftMs: 60000, // 1 minute max allowed drift
|
|
31
|
+
validateBeforeSync: true,
|
|
32
|
+
blockOnFailure: true,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
/**
|
|
37
|
+
* Get enforcement settings
|
|
38
|
+
*/
|
|
39
|
+
async getSettings() {
|
|
40
|
+
const store = getStore();
|
|
41
|
+
const data = await store.get({ key: STORE_KEY });
|
|
42
|
+
return { ...DEFAULT_ENFORCEMENT_SETTINGS, ...data };
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Update enforcement settings
|
|
47
|
+
*/
|
|
48
|
+
async updateSettings(updates) {
|
|
49
|
+
const store = getStore();
|
|
50
|
+
const current = await this.getSettings();
|
|
51
|
+
const newSettings = { ...current, ...updates };
|
|
52
|
+
|
|
53
|
+
// Validate settings
|
|
54
|
+
if (newSettings.schemaMatchMode && !['strict', 'compatible', 'none'].includes(newSettings.schemaMatchMode)) {
|
|
55
|
+
throw new Error(`Invalid schema match mode: ${newSettings.schemaMatchMode}`);
|
|
56
|
+
}
|
|
57
|
+
if (newSettings.allowedVersionDrift && !['exact', 'minor', 'major', 'none'].includes(newSettings.allowedVersionDrift)) {
|
|
58
|
+
throw new Error(`Invalid version drift mode: ${newSettings.allowedVersionDrift}`);
|
|
59
|
+
}
|
|
60
|
+
if (newSettings.maxTimeDriftMs !== undefined && (newSettings.maxTimeDriftMs < 0 || newSettings.maxTimeDriftMs > 86400000)) {
|
|
61
|
+
throw new Error('Max time drift must be between 0 and 86400000 ms (24 hours)');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await store.set({ key: STORE_KEY, value: newSettings });
|
|
65
|
+
return newSettings;
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get local Strapi version info
|
|
70
|
+
*/
|
|
71
|
+
getLocalVersionInfo() {
|
|
72
|
+
return {
|
|
73
|
+
strapiVersion: strapi.config.info?.strapi || 'unknown',
|
|
74
|
+
nodeVersion: process.version,
|
|
75
|
+
serverTime: new Date().toISOString(),
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get local content type schema for comparison
|
|
81
|
+
*/
|
|
82
|
+
getLocalSchema(uid) {
|
|
83
|
+
const contentType = strapi.contentTypes[uid];
|
|
84
|
+
if (!contentType) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const attributes = contentType.attributes || {};
|
|
89
|
+
const schema = {};
|
|
90
|
+
|
|
91
|
+
for (const [field, attr] of Object.entries(attributes)) {
|
|
92
|
+
schema[field] = {
|
|
93
|
+
type: attr.type,
|
|
94
|
+
required: attr.required || false,
|
|
95
|
+
unique: attr.unique || false,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
if (attr.type === 'relation') {
|
|
99
|
+
schema[field].relation = attr.relation;
|
|
100
|
+
schema[field].target = attr.target;
|
|
101
|
+
}
|
|
102
|
+
if (attr.type === 'enumeration') {
|
|
103
|
+
schema[field].enum = attr.enum;
|
|
104
|
+
}
|
|
105
|
+
if (attr.type === 'component') {
|
|
106
|
+
schema[field].component = attr.component;
|
|
107
|
+
schema[field].repeatable = attr.repeatable;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return schema;
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Compare two schemas for compatibility
|
|
116
|
+
*/
|
|
117
|
+
compareSchemas(localSchema, remoteSchema, mode = 'strict') {
|
|
118
|
+
const result = {
|
|
119
|
+
compatible: true,
|
|
120
|
+
missingLocal: [],
|
|
121
|
+
missingRemote: [],
|
|
122
|
+
typeMismatches: [],
|
|
123
|
+
warnings: [],
|
|
124
|
+
differences: [],
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
if (!localSchema || !remoteSchema) {
|
|
128
|
+
result.compatible = false;
|
|
129
|
+
result.warnings.push('One or both schemas are missing');
|
|
130
|
+
result.differences.push('Missing schema');
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const localFields = new Set(Object.keys(localSchema));
|
|
135
|
+
const remoteFields = new Set(Object.keys(remoteSchema));
|
|
136
|
+
|
|
137
|
+
// Check for missing fields
|
|
138
|
+
for (const field of localFields) {
|
|
139
|
+
if (!remoteFields.has(field)) {
|
|
140
|
+
result.missingRemote.push(field);
|
|
141
|
+
result.differences.push(`Field "${field}" missing on remote`);
|
|
142
|
+
if (mode === 'strict') {
|
|
143
|
+
result.compatible = false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const field of remoteFields) {
|
|
149
|
+
if (!localFields.has(field)) {
|
|
150
|
+
result.missingLocal.push(field);
|
|
151
|
+
result.differences.push(`Field "${field}" missing locally`);
|
|
152
|
+
if (mode === 'strict') {
|
|
153
|
+
result.compatible = false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check type mismatches for common fields
|
|
159
|
+
for (const field of localFields) {
|
|
160
|
+
if (remoteFields.has(field)) {
|
|
161
|
+
const local = localSchema[field];
|
|
162
|
+
const remote = remoteSchema[field];
|
|
163
|
+
|
|
164
|
+
if (local.type !== remote.type) {
|
|
165
|
+
result.typeMismatches.push({
|
|
166
|
+
field,
|
|
167
|
+
localType: local.type,
|
|
168
|
+
remoteType: remote.type,
|
|
169
|
+
});
|
|
170
|
+
result.differences.push(`Field "${field}" type mismatch: ${local.type} vs ${remote.type}`);
|
|
171
|
+
result.compatible = false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Additional checks for relations
|
|
175
|
+
if (local.type === 'relation' && remote.type === 'relation') {
|
|
176
|
+
if (local.relation !== remote.relation) {
|
|
177
|
+
result.warnings.push(`Relation type mismatch for field "${field}": ${local.relation} vs ${remote.relation}`);
|
|
178
|
+
result.differences.push(`Field "${field}" relation mismatch`);
|
|
179
|
+
if (mode === 'strict') {
|
|
180
|
+
result.compatible = false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return result;
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Compare version strings
|
|
192
|
+
*/
|
|
193
|
+
compareVersions(localVersion, remoteVersion, allowedDrift = 'minor') {
|
|
194
|
+
if (allowedDrift === 'none') {
|
|
195
|
+
return { compatible: true, message: 'Version check disabled' };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const parseVersion = (v) => {
|
|
199
|
+
const match = v.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
200
|
+
if (!match) return null;
|
|
201
|
+
return {
|
|
202
|
+
major: parseInt(match[1], 10),
|
|
203
|
+
minor: parseInt(match[2], 10),
|
|
204
|
+
patch: parseInt(match[3], 10),
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const local = parseVersion(localVersion);
|
|
209
|
+
const remote = parseVersion(remoteVersion);
|
|
210
|
+
|
|
211
|
+
if (!local || !remote) {
|
|
212
|
+
return {
|
|
213
|
+
compatible: false,
|
|
214
|
+
message: `Unable to parse versions: local=${localVersion}, remote=${remoteVersion}`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const result = { compatible: true, message: '', driftLevel: 'none' };
|
|
219
|
+
|
|
220
|
+
switch (allowedDrift) {
|
|
221
|
+
case 'exact':
|
|
222
|
+
result.compatible = (
|
|
223
|
+
local.major === remote.major &&
|
|
224
|
+
local.minor === remote.minor &&
|
|
225
|
+
local.patch === remote.patch
|
|
226
|
+
);
|
|
227
|
+
result.driftLevel = result.compatible ? 'none' : 'patch';
|
|
228
|
+
result.message = result.compatible ? 'Versions match exactly' : 'Versions must match exactly';
|
|
229
|
+
break;
|
|
230
|
+
|
|
231
|
+
case 'minor':
|
|
232
|
+
result.compatible = local.major === remote.major;
|
|
233
|
+
result.driftLevel = local.major !== remote.major ? 'major' : (local.minor !== remote.minor ? 'minor' : 'patch');
|
|
234
|
+
result.message = result.compatible
|
|
235
|
+
? 'Major versions match'
|
|
236
|
+
: `Major version mismatch: ${local.major} vs ${remote.major}`;
|
|
237
|
+
break;
|
|
238
|
+
|
|
239
|
+
case 'major':
|
|
240
|
+
result.compatible = true; // Allow any version
|
|
241
|
+
result.driftLevel = local.major !== remote.major ? 'major' : (local.minor !== remote.minor ? 'minor' : 'patch');
|
|
242
|
+
result.message = 'Major version drift allowed';
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return result;
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Check time synchronization between instances
|
|
251
|
+
*/
|
|
252
|
+
checkTimeSync(localTime, remoteTime, maxDriftMs = 60000) {
|
|
253
|
+
const localDate = new Date(localTime);
|
|
254
|
+
const remoteDate = new Date(remoteTime);
|
|
255
|
+
const drift = Math.abs(localDate.getTime() - remoteDate.getTime());
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
compatible: drift <= maxDriftMs,
|
|
259
|
+
drift,
|
|
260
|
+
maxAllowed: maxDriftMs,
|
|
261
|
+
message: drift <= maxDriftMs
|
|
262
|
+
? `Time drift ${drift}ms is within allowed ${maxDriftMs}ms`
|
|
263
|
+
: `Time drift ${drift}ms exceeds allowed ${maxDriftMs}ms`,
|
|
264
|
+
};
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Run all enforcement checks before sync
|
|
269
|
+
*/
|
|
270
|
+
async runPreSyncChecks(contentTypeUid, remoteInfo) {
|
|
271
|
+
const settings = await this.getSettings();
|
|
272
|
+
const results = {
|
|
273
|
+
passed: true,
|
|
274
|
+
checks: [],
|
|
275
|
+
errors: [],
|
|
276
|
+
warnings: [],
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// Version check
|
|
280
|
+
if (settings.enforceVersionCheck) {
|
|
281
|
+
const localVersion = this.getLocalVersionInfo();
|
|
282
|
+
const versionCheck = this.compareVersions(
|
|
283
|
+
localVersion.strapi,
|
|
284
|
+
remoteInfo.strapi || 'unknown',
|
|
285
|
+
settings.allowedVersionDrift
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
results.checks.push({
|
|
289
|
+
name: 'version',
|
|
290
|
+
passed: versionCheck.compatible,
|
|
291
|
+
message: versionCheck.message,
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
if (!versionCheck.compatible && settings.blockOnFailure) {
|
|
295
|
+
results.passed = false;
|
|
296
|
+
results.errors.push(`Version check failed: ${versionCheck.message}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Schema check
|
|
301
|
+
if (settings.enforceSchemaMatch && settings.schemaMatchMode !== 'none') {
|
|
302
|
+
const localSchema = this.getLocalSchema(contentTypeUid);
|
|
303
|
+
const remoteSchema = remoteInfo.schema;
|
|
304
|
+
const schemaCheck = this.compareSchemas(localSchema, remoteSchema, settings.schemaMatchMode);
|
|
305
|
+
|
|
306
|
+
results.checks.push({
|
|
307
|
+
name: 'schema',
|
|
308
|
+
passed: schemaCheck.compatible,
|
|
309
|
+
details: schemaCheck,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
if (!schemaCheck.compatible && settings.blockOnFailure) {
|
|
313
|
+
results.passed = false;
|
|
314
|
+
results.errors.push(`Schema check failed for ${contentTypeUid}`);
|
|
315
|
+
if (schemaCheck.typeMismatches.length > 0) {
|
|
316
|
+
results.errors.push(`Type mismatches: ${schemaCheck.typeMismatches.map(m => m.field).join(', ')}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
results.warnings.push(...schemaCheck.warnings);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Time sync check
|
|
324
|
+
if (settings.enforceDateTimeSync) {
|
|
325
|
+
const localTime = new Date().toISOString();
|
|
326
|
+
const timeCheck = this.checkTimeSync(localTime, remoteInfo.timestamp, settings.maxTimeDriftMs);
|
|
327
|
+
|
|
328
|
+
results.checks.push({
|
|
329
|
+
name: 'timeSync',
|
|
330
|
+
passed: timeCheck.compatible,
|
|
331
|
+
message: timeCheck.message,
|
|
332
|
+
drift: timeCheck.drift,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
if (!timeCheck.compatible && settings.blockOnFailure) {
|
|
336
|
+
results.passed = false;
|
|
337
|
+
results.errors.push(`Time sync check failed: ${timeCheck.message}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return results;
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Get enforcement summary for UI
|
|
346
|
+
*/
|
|
347
|
+
async getEnforcementSummary() {
|
|
348
|
+
const settings = await this.getSettings();
|
|
349
|
+
const localVersion = this.getLocalVersionInfo();
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
settings,
|
|
353
|
+
localInfo: localVersion,
|
|
354
|
+
checksEnabled: {
|
|
355
|
+
schema: settings.enforceSchemaMatch && settings.schemaMatchMode !== 'none',
|
|
356
|
+
version: settings.enforceVersionCheck && settings.allowedVersionDrift !== 'none',
|
|
357
|
+
timeSync: settings.enforceDateTimeSync,
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
};
|