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
|
@@ -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
|
-
* @
|
|
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
|
-
|
|
32
|
+
const k = keyOf(r);
|
|
33
|
+
if (k) localBySyncId.set(k, r);
|
|
28
34
|
}
|
|
29
35
|
for (const r of remoteRecords) {
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|