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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +206 -0
  3. package/admin/src/components/ConfigTab.jsx +1038 -0
  4. package/admin/src/components/ContentTypesTab.jsx +160 -0
  5. package/admin/src/components/HelpTab.jsx +945 -0
  6. package/admin/src/components/LogsTab.jsx +136 -0
  7. package/admin/src/components/MediaTab.jsx +557 -0
  8. package/admin/src/components/SyncProfilesTab.jsx +715 -0
  9. package/admin/src/components/SyncTab.jsx +988 -0
  10. package/admin/src/index.js +31 -0
  11. package/admin/src/pages/App/index.jsx +129 -0
  12. package/admin/src/pluginId.js +3 -0
  13. package/package.json +84 -0
  14. package/server/src/bootstrap.js +151 -0
  15. package/server/src/config/index.js +5 -0
  16. package/server/src/content-types/index.js +7 -0
  17. package/server/src/content-types/sync-log/schema.json +24 -0
  18. package/server/src/controllers/alerts.js +59 -0
  19. package/server/src/controllers/config.js +292 -0
  20. package/server/src/controllers/content-type-discovery.js +9 -0
  21. package/server/src/controllers/dependencies.js +109 -0
  22. package/server/src/controllers/index.js +29 -0
  23. package/server/src/controllers/ping.js +7 -0
  24. package/server/src/controllers/sync-config.js +26 -0
  25. package/server/src/controllers/sync-enforcement.js +323 -0
  26. package/server/src/controllers/sync-execution.js +134 -0
  27. package/server/src/controllers/sync-log.js +18 -0
  28. package/server/src/controllers/sync-media.js +158 -0
  29. package/server/src/controllers/sync-profiles.js +182 -0
  30. package/server/src/controllers/sync.js +31 -0
  31. package/server/src/destroy.js +7 -0
  32. package/server/src/index.js +21 -0
  33. package/server/src/middlewares/verify-signature.js +32 -0
  34. package/server/src/register.js +7 -0
  35. package/server/src/routes/index.js +111 -0
  36. package/server/src/services/alerts.js +437 -0
  37. package/server/src/services/config.js +68 -0
  38. package/server/src/services/content-type-discovery.js +41 -0
  39. package/server/src/services/dependency-resolver.js +284 -0
  40. package/server/src/services/index.js +30 -0
  41. package/server/src/services/ping.js +7 -0
  42. package/server/src/services/sync-config.js +45 -0
  43. package/server/src/services/sync-enforcement.js +362 -0
  44. package/server/src/services/sync-execution.js +541 -0
  45. package/server/src/services/sync-log.js +56 -0
  46. package/server/src/services/sync-media.js +963 -0
  47. package/server/src/services/sync-profiles.js +380 -0
  48. package/server/src/services/sync.js +248 -0
  49. package/server/src/utils/applier.js +89 -0
  50. package/server/src/utils/comparator.js +83 -0
  51. package/server/src/utils/fetcher.js +142 -0
  52. package/server/src/utils/hmac.js +37 -0
  53. package/server/src/utils/pagination.js +51 -0
  54. package/server/src/utils/sync-guard.js +29 -0
  55. 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
+ };