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,284 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Dependency Resolver Service
5
+ *
6
+ * Analyzes content type relationships and resolves dependencies for sync operations.
7
+ * Ensures related entities are synced before or alongside core entities.
8
+ *
9
+ * Handles:
10
+ * - Relations (oneToOne, oneToMany, manyToOne, manyToMany)
11
+ * - Components
12
+ * - Dynamic zones
13
+ * - Media (via upload plugin)
14
+ */
15
+ module.exports = ({ strapi }) => {
16
+ // Cache for resolved dependency graphs
17
+ const dependencyCache = new Map();
18
+
19
+ return {
20
+ /**
21
+ * Get content type schema with relation details
22
+ */
23
+ getContentTypeSchema(uid) {
24
+ const contentType = strapi.contentTypes[uid];
25
+ if (!contentType) {
26
+ throw new Error(`Content type "${uid}" not found`);
27
+ }
28
+ return contentType;
29
+ },
30
+
31
+ /**
32
+ * Analyze a content type and extract all dependencies
33
+ */
34
+ analyzeContentType(uid) {
35
+ const cached = dependencyCache.get(uid);
36
+ if (cached) {
37
+ return cached;
38
+ }
39
+
40
+ const contentType = this.getContentTypeSchema(uid);
41
+ const attributes = contentType.attributes || {};
42
+ const dependencies = {
43
+ uid,
44
+ relations: [],
45
+ components: [],
46
+ dynamicZones: [],
47
+ media: [],
48
+ };
49
+
50
+ for (const [fieldName, attr] of Object.entries(attributes)) {
51
+ switch (attr.type) {
52
+ case 'relation':
53
+ if (attr.target) {
54
+ dependencies.relations.push({
55
+ field: fieldName,
56
+ target: attr.target,
57
+ relation: attr.relation,
58
+ mappedBy: attr.mappedBy,
59
+ inversedBy: attr.inversedBy,
60
+ });
61
+ }
62
+ break;
63
+
64
+ case 'component':
65
+ dependencies.components.push({
66
+ field: fieldName,
67
+ component: attr.component,
68
+ repeatable: attr.repeatable || false,
69
+ });
70
+ break;
71
+
72
+ case 'dynamiczone':
73
+ dependencies.dynamicZones.push({
74
+ field: fieldName,
75
+ components: attr.components || [],
76
+ });
77
+ break;
78
+
79
+ case 'media':
80
+ dependencies.media.push({
81
+ field: fieldName,
82
+ multiple: attr.multiple || false,
83
+ allowedTypes: attr.allowedTypes || ['images', 'files', 'videos', 'audios'],
84
+ });
85
+ break;
86
+ }
87
+ }
88
+
89
+ dependencyCache.set(uid, dependencies);
90
+ return dependencies;
91
+ },
92
+
93
+ /**
94
+ * Build a full dependency graph for a content type
95
+ * Returns ordered list of content types that need to be synced
96
+ */
97
+ buildDependencyGraph(uid, depth = 1, visited = new Set()) {
98
+ if (depth < 1 || visited.has(uid)) {
99
+ return [];
100
+ }
101
+
102
+ visited.add(uid);
103
+ const analysis = this.analyzeContentType(uid);
104
+ const graph = [];
105
+
106
+ // Process relations (other content types)
107
+ for (const relation of analysis.relations) {
108
+ const targetUid = relation.target;
109
+
110
+ // Skip self-references and already visited
111
+ if (targetUid === uid || visited.has(targetUid)) {
112
+ continue;
113
+ }
114
+
115
+ // Skip plugin content types (admin, upload, etc.) unless explicitly synced
116
+ if (targetUid.startsWith('plugin::') && !targetUid.startsWith('plugin::users-permissions')) {
117
+ continue;
118
+ }
119
+
120
+ // Add to graph with lower priority (dependencies first)
121
+ graph.push({
122
+ uid: targetUid,
123
+ referencedBy: uid,
124
+ field: relation.field,
125
+ relationType: relation.relation,
126
+ priority: 1, // Lower number = sync first
127
+ });
128
+
129
+ // Recurse if depth allows
130
+ if (depth > 1) {
131
+ const subGraph = this.buildDependencyGraph(targetUid, depth - 1, visited);
132
+ for (const entry of subGraph) {
133
+ entry.priority += 1; // Increase priority (sync even earlier)
134
+ graph.push(entry);
135
+ }
136
+ }
137
+ }
138
+
139
+ // Process components (shared structures)
140
+ for (const comp of analysis.components) {
141
+ const compUid = comp.component;
142
+ if (!visited.has(compUid)) {
143
+ visited.add(compUid);
144
+ graph.push({
145
+ uid: compUid,
146
+ referencedBy: uid,
147
+ field: comp.field,
148
+ type: 'component',
149
+ repeatable: comp.repeatable,
150
+ priority: 0, // Components sync first
151
+ });
152
+ }
153
+ }
154
+
155
+ // Process dynamic zones
156
+ for (const dz of analysis.dynamicZones) {
157
+ for (const compUid of dz.components) {
158
+ if (!visited.has(compUid)) {
159
+ visited.add(compUid);
160
+ graph.push({
161
+ uid: compUid,
162
+ referencedBy: uid,
163
+ field: dz.field,
164
+ type: 'dynamiczone_component',
165
+ priority: 0,
166
+ });
167
+ }
168
+ }
169
+ }
170
+
171
+ return graph;
172
+ },
173
+
174
+ /**
175
+ * Get ordered list of content types to sync for a given content type
176
+ * Returns UIDs in the order they should be synced (dependencies first)
177
+ */
178
+ getSyncOrder(uid, depth = 1) {
179
+ const graph = this.buildDependencyGraph(uid, depth);
180
+
181
+ // Sort by priority (lower first) and deduplicate
182
+ const sorted = graph
183
+ .sort((a, b) => a.priority - b.priority)
184
+ .reduce((acc, entry) => {
185
+ if (!acc.some(e => e.uid === entry.uid)) {
186
+ acc.push(entry);
187
+ }
188
+ return acc;
189
+ }, []);
190
+
191
+ // Return just the UIDs in order, followed by the main content type
192
+ const order = sorted.map(e => e.uid);
193
+ order.push(uid);
194
+
195
+ return order;
196
+ },
197
+
198
+ /**
199
+ * Extract related entity IDs from a record for dependency syncing
200
+ */
201
+ extractRelatedIds(record, uid) {
202
+ const analysis = this.analyzeContentType(uid);
203
+ const relatedIds = {};
204
+
205
+ for (const relation of analysis.relations) {
206
+ const fieldValue = record[relation.field];
207
+ if (!fieldValue) continue;
208
+
209
+ const ids = [];
210
+ if (Array.isArray(fieldValue)) {
211
+ // Many relation
212
+ for (const item of fieldValue) {
213
+ if (item && (item.id || item.documentId)) {
214
+ ids.push({
215
+ id: item.id,
216
+ documentId: item.documentId,
217
+ });
218
+ }
219
+ }
220
+ } else if (typeof fieldValue === 'object') {
221
+ // Single relation
222
+ if (fieldValue.id || fieldValue.documentId) {
223
+ ids.push({
224
+ id: fieldValue.id,
225
+ documentId: fieldValue.documentId,
226
+ });
227
+ }
228
+ } else if (typeof fieldValue === 'number' || typeof fieldValue === 'string') {
229
+ // Just an ID reference
230
+ ids.push({ id: fieldValue });
231
+ }
232
+
233
+ if (ids.length > 0) {
234
+ relatedIds[relation.target] = relatedIds[relation.target] || [];
235
+ relatedIds[relation.target].push(...ids);
236
+ }
237
+ }
238
+
239
+ return relatedIds;
240
+ },
241
+
242
+ /**
243
+ * Check if a content type has syncable dependencies
244
+ */
245
+ hasDependencies(uid) {
246
+ const analysis = this.analyzeContentType(uid);
247
+ return (
248
+ analysis.relations.length > 0 ||
249
+ analysis.components.length > 0 ||
250
+ analysis.dynamicZones.length > 0
251
+ );
252
+ },
253
+
254
+ /**
255
+ * Get dependency summary for UI display
256
+ */
257
+ getDependencySummary(uid, depth = 1) {
258
+ const analysis = this.analyzeContentType(uid);
259
+ const graph = this.buildDependencyGraph(uid, depth);
260
+
261
+ return {
262
+ uid,
263
+ directRelations: analysis.relations.length,
264
+ components: analysis.components.length,
265
+ dynamicZones: analysis.dynamicZones.length,
266
+ mediaFields: analysis.media.length,
267
+ totalDependencies: graph.length,
268
+ dependencies: graph.map(d => ({
269
+ uid: d.uid,
270
+ field: d.field,
271
+ type: d.type || 'relation',
272
+ relationType: d.relationType,
273
+ })),
274
+ };
275
+ },
276
+
277
+ /**
278
+ * Clear the dependency cache (call after schema changes)
279
+ */
280
+ clearCache() {
281
+ dependencyCache.clear();
282
+ },
283
+ };
284
+ };
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ const ping = require('./ping');
4
+ const config = require('./config');
5
+ const contentTypeDiscovery = require('./content-type-discovery');
6
+ const syncConfig = require('./sync-config');
7
+ const sync = require('./sync');
8
+ const syncLog = require('./sync-log');
9
+ const syncProfiles = require('./sync-profiles');
10
+ const syncExecution = require('./sync-execution');
11
+ const dependencyResolver = require('./dependency-resolver');
12
+ const syncEnforcement = require('./sync-enforcement');
13
+ const syncMedia = require('./sync-media');
14
+ const alerts = require('./alerts');
15
+
16
+ module.exports = {
17
+ ping,
18
+ config,
19
+ contentTypeDiscovery,
20
+ syncConfig,
21
+ sync,
22
+ syncLog,
23
+ syncProfiles,
24
+ syncExecution,
25
+ dependencyResolver,
26
+ syncEnforcement,
27
+ syncMedia,
28
+ alerts,
29
+ };
30
+
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ module.exports = ({ strapi }) => ({
4
+ getStatus() {
5
+ return { status: 'ok' };
6
+ },
7
+ });
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ const STORE_KEY = 'sync-configuration';
4
+
5
+ module.exports = ({ strapi }) => {
6
+ function getStore() {
7
+ return strapi.store({ type: 'plugin', name: 'strapi-content-sync-pro' });
8
+ }
9
+
10
+ return {
11
+ async getSyncConfig() {
12
+ const store = getStore();
13
+ const data = await store.get({ key: STORE_KEY });
14
+ return data || { contentTypes: [], conflictStrategy: 'latest' };
15
+ },
16
+
17
+ async setSyncConfig(config) {
18
+ const store = getStore();
19
+
20
+ if (!config.contentTypes || !Array.isArray(config.contentTypes)) {
21
+ throw new Error('contentTypes must be an array');
22
+ }
23
+
24
+ for (const ct of config.contentTypes) {
25
+ if (!ct.uid) throw new Error('Each content type must have a uid');
26
+ if (ct.direction && !['push', 'pull', 'both'].includes(ct.direction)) {
27
+ throw new Error(`Invalid direction "${ct.direction}" for ${ct.uid}`);
28
+ }
29
+ }
30
+
31
+ const value = {
32
+ contentTypes: config.contentTypes.map((ct) => ({
33
+ uid: ct.uid,
34
+ direction: ct.direction || 'both',
35
+ fields: ct.fields || [],
36
+ enabled: ct.enabled !== undefined ? ct.enabled : true,
37
+ })),
38
+ conflictStrategy: config.conflictStrategy || 'latest',
39
+ };
40
+
41
+ await store.set({ key: STORE_KEY, value });
42
+ return value;
43
+ },
44
+ };
45
+ };