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.
Files changed (48) hide show
  1. package/README.md +84 -25
  2. package/admin/src/components/ConfigTab.jsx +29 -6
  3. package/admin/src/components/HelpTab.jsx +131 -32
  4. package/admin/src/components/MediaTab.jsx +7 -0
  5. package/admin/src/components/StatsTab.jsx +470 -0
  6. package/admin/src/components/SyncProfilesTab.jsx +63 -5
  7. package/admin/src/components/SyncTab.jsx +51 -7
  8. package/admin/src/pages/App/index.jsx +3 -0
  9. package/docs/Screenshot 2026-04-20 160506.png +0 -0
  10. package/docs/Screenshot 2026-04-20 160558.png +0 -0
  11. package/docs/Screenshot 2026-04-20 175903.png +0 -0
  12. package/docs/Screenshot 2026-04-20 175931.png +0 -0
  13. package/docs/Screenshot 2026-04-20 180001.png +0 -0
  14. package/docs/Screenshot 2026-04-20 180041.png +0 -0
  15. package/docs/Screenshot 2026-04-20 180116.png +0 -0
  16. package/docs/Screenshot 2026-04-20 180135.png +0 -0
  17. package/docs/Screenshot 2026-04-20 180202.png +0 -0
  18. package/docs/Screenshot 2026-04-20 180228.png +0 -0
  19. package/docs/Screenshot 2026-04-20 180251.png +0 -0
  20. package/docs/Screenshot 2026-04-20 180301.png +0 -0
  21. package/docs/clipchamp-screen-recording-script.md +0 -0
  22. package/docs/logo-horizontal.svg +33 -0
  23. package/docs/logo-mark.svg +38 -0
  24. package/docs/logo-square.svg +27 -0
  25. package/docs/production-readiness-status.md +34 -0
  26. package/docs/production-readiness-test-matrix.md +151 -0
  27. package/docs/test-environments-setup-legacy.txt +60 -0
  28. package/package.json +2 -1
  29. package/server/src/content-types/index.js +2 -0
  30. package/server/src/content-types/sync-run-report/schema.json +26 -0
  31. package/server/src/controllers/config.js +48 -5
  32. package/server/src/controllers/index.js +2 -0
  33. package/server/src/controllers/sync-log.js +6 -0
  34. package/server/src/controllers/sync-media.js +19 -0
  35. package/server/src/controllers/sync-stats.js +51 -0
  36. package/server/src/controllers/sync.js +9 -3
  37. package/server/src/routes/index.js +13 -0
  38. package/server/src/services/config.js +18 -2
  39. package/server/src/services/index.js +2 -0
  40. package/server/src/services/sync-execution.js +102 -5
  41. package/server/src/services/sync-log.js +36 -0
  42. package/server/src/services/sync-media.js +224 -1
  43. package/server/src/services/sync-profiles.js +92 -4
  44. package/server/src/services/sync-stats.js +353 -0
  45. package/server/src/services/sync.js +324 -97
  46. package/server/src/utils/applier.js +120 -13
  47. package/server/src/utils/comparator.js +22 -6
  48. package/server/src/utils/fetcher.js +11 -2
@@ -8,26 +8,33 @@
8
8
  * @param {Object} options
9
9
  * @param {string} options.direction – "push" | "pull" | "both"
10
10
  * @param {string} options.conflictStrategy – "latest" | "local_wins" | "remote_wins"
11
- * @returns {{ toPush, toPull, toCreateRemote, toCreateLocal }}
11
+ * @param {boolean} options.syncDeletions – propagate missing records as deletions
12
+ * @returns {{ toPush, toPull, toCreateRemote, toCreateLocal, toDeleteRemote, toDeleteLocal }}
12
13
  */
13
14
  function compareRecords(localRecords, remoteRecords, options = {}) {
14
- const { direction = 'both', conflictStrategy = 'latest' } = options;
15
+ const { direction = 'both', conflictStrategy = 'latest', syncDeletions = false } = options;
15
16
 
16
17
  const result = {
17
18
  toPush: [],
18
19
  toPull: [],
19
20
  toCreateRemote: [],
20
21
  toCreateLocal: [],
22
+ toDeleteRemote: [],
23
+ toDeleteLocal: [],
21
24
  };
22
25
 
23
26
  const localBySyncId = new Map();
24
27
  const remoteBySyncId = new Map();
25
28
 
29
+ const keyOf = (r) => r && (r.documentId || r.syncId);
30
+
26
31
  for (const r of localRecords) {
27
- if (r.syncId) localBySyncId.set(r.syncId, r);
32
+ const k = keyOf(r);
33
+ if (k) localBySyncId.set(k, r);
28
34
  }
29
35
  for (const r of remoteRecords) {
30
- if (r.syncId) remoteBySyncId.set(r.syncId, r);
36
+ const k = keyOf(r);
37
+ if (k) remoteBySyncId.set(k, r);
31
38
  }
32
39
 
33
40
  // Records that exist on both sides
@@ -43,7 +50,11 @@ function compareRecords(localRecords, remoteRecords, options = {}) {
43
50
  result.toPull.push({ local: localRecord, remote: remoteRecord });
44
51
  }
45
52
  } else if (direction === 'push' || direction === 'both') {
46
- result.toCreateRemote.push(localRecord);
53
+ if (syncDeletions && direction !== 'both') {
54
+ result.toDeleteRemote.push(localRecord);
55
+ } else {
56
+ result.toCreateRemote.push(localRecord);
57
+ }
47
58
  }
48
59
  }
49
60
 
@@ -51,7 +62,12 @@ function compareRecords(localRecords, remoteRecords, options = {}) {
51
62
  for (const [syncId] of remoteBySyncId) {
52
63
  if (!localBySyncId.has(syncId)) {
53
64
  if (direction === 'pull' || direction === 'both') {
54
- result.toCreateLocal.push(remoteBySyncId.get(syncId));
65
+ const remoteRecord = remoteBySyncId.get(syncId);
66
+ if (syncDeletions && direction !== 'both') {
67
+ result.toDeleteLocal.push(remoteRecord);
68
+ } else {
69
+ result.toCreateLocal.push(remoteRecord);
70
+ }
55
71
  }
56
72
  }
57
73
  }
@@ -19,7 +19,7 @@ async function fetchLocalPage(strapi, uid, { fields, lastSyncAt, page = 1, pageS
19
19
  }
20
20
 
21
21
  if (fields && fields.length > 0) {
22
- params.fields = [...new Set([...fields, 'syncId', 'updatedAt'])];
22
+ params.fields = [...new Set([...fields, 'documentId', 'syncId', 'updatedAt'])];
23
23
  }
24
24
 
25
25
  const records = (await strapi.documents(uid).findMany(params)) || [];
@@ -37,7 +37,7 @@ async function fetchRemotePage(remoteConfig, uid, { fields, lastSyncAt, page = 1
37
37
  const url = new URL(`/api/${pluralName}`, baseUrl);
38
38
 
39
39
  if (fields && fields.length > 0) {
40
- const allFields = [...new Set([...fields, 'syncId', 'updatedAt'])];
40
+ const allFields = [...new Set([...fields, 'documentId', 'syncId', 'updatedAt'])];
41
41
  allFields.forEach((f, i) => {
42
42
  url.searchParams.set(`fields[${i}]`, f);
43
43
  });
@@ -123,10 +123,19 @@ async function fetchRemoteRecords(remoteConfig, uid, options = {}) {
123
123
  * e.g. "api::product.product" → "products"
124
124
  */
125
125
  function uidToPluralEndpoint(uid) {
126
+ const contentType = global.strapi?.contentTypes?.[uid];
127
+ const configuredPlural = contentType?.info?.pluralName;
128
+ if (configuredPlural) {
129
+ return configuredPlural;
130
+ }
131
+
126
132
  const parts = uid.split('.');
127
133
  const modelName = parts[parts.length - 1];
128
134
  if (modelName.endsWith('s')) return modelName;
129
135
  if (modelName.endsWith('y')) return modelName.slice(0, -1) + 'ies';
136
+ if (modelName.endsWith('ch') || modelName.endsWith('sh') || modelName.endsWith('x') || modelName.endsWith('z')) {
137
+ return modelName + 'es';
138
+ }
130
139
  return modelName + 's';
131
140
  }
132
141