strapi-content-sync-pro 1.0.4 → 1.0.6
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 +1 -1
- package/README.md +32 -14
- package/admin/src/components/BulkTransferTab.jsx +185 -20
- package/admin/src/components/ConfigTab.jsx +81 -3
- package/admin/src/components/ContentTypesTab.jsx +28 -1
- package/admin/src/components/HelpTab.jsx +34 -0
- package/admin/src/components/LogsTab.jsx +66 -8
- package/admin/src/components/MediaTab.jsx +253 -36
- package/admin/src/components/SyncProfilesTab.jsx +140 -4
- package/admin/src/components/SyncTab.jsx +161 -35
- package/docs/Screenshot 2026-04-22 183540.png +0 -0
- package/docs/Screenshot 2026-04-22 183552.png +0 -0
- package/docs/Screenshot 2026-04-23 114332.png +0 -0
- package/docs/Screenshot 2026-04-23 114644.png +0 -0
- package/docs/Screenshot 2026-04-23 114651.png +0 -0
- package/docs/Screenshot 2026-04-23 114737.png +0 -0
- package/docs/Screenshot 2026-04-23 114904.png +0 -0
- package/docs/Screenshot 2026-04-23 114940.png +0 -0
- package/docs/Screenshot 2026-04-23 115003.png +0 -0
- package/docs/Screenshot 2026-04-23 115024.png +0 -0
- package/docs/Screenshot 2026-04-23 115116.png +0 -0
- package/docs/Screenshot 2026-04-23 115141.png +0 -0
- package/docs/Screenshot 2026-04-23 115252.png +0 -0
- package/docs/Screenshot 2026-04-23 115448.png +0 -0
- package/docs/Screenshot 2026-04-23 120534.png +0 -0
- package/docs/Screenshot 2026-04-23 122544.png +0 -0
- package/docs/Screenshot 2026-04-23 122712.png +0 -0
- package/docs/Screenshot 2026-04-23 122730.png +0 -0
- package/docs/Screenshot 2026-04-23 122858.png +0 -0
- package/docs/Screenshot 2026-04-23 122924.png +0 -0
- package/docs/Screenshot 2026-04-23 122937.png +0 -0
- package/docs/sync-strategy-approach-review.md +127 -0
- package/package.json +1 -1
- package/server/src/controllers/config.js +76 -3
- package/server/src/controllers/sync-media.js +24 -0
- package/server/src/routes/index.js +3 -0
- package/server/src/services/bulk-transfer.js +45 -1
- package/server/src/services/dependency-resolver.js +37 -0
- package/server/src/services/sync-execution.js +21 -9
- package/server/src/services/sync-media.js +168 -32
- package/server/src/services/sync-profiles.js +36 -15
- package/server/src/services/sync.js +234 -134
- package/server/src/utils/fetcher.js +7 -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/production-readiness-status.md +0 -34
- package/docs/production-readiness-test-matrix.md +0 -151
- package/docs/test-environments-setup-legacy.txt +0 -60
|
@@ -147,6 +147,64 @@ module.exports = ({ strapi }) => {
|
|
|
147
147
|
const log = strapi.log;
|
|
148
148
|
const schedulerHandles = {};
|
|
149
149
|
|
|
150
|
+
// ── In-memory run controls (pause/resume/cancel) per profile ─────────────
|
|
151
|
+
// Key: profileId, Value: { signal: 'run' | 'pause' | 'cancel',
|
|
152
|
+
// resumeDeferred: { promise, resolve } | null,
|
|
153
|
+
// progress: { phase, processed, total, pushed, pulled, skipped, errors } }
|
|
154
|
+
const runControls = {};
|
|
155
|
+
|
|
156
|
+
function getControl(profileId) {
|
|
157
|
+
if (!runControls[profileId]) {
|
|
158
|
+
runControls[profileId] = {
|
|
159
|
+
signal: 'run',
|
|
160
|
+
resumeDeferred: null,
|
|
161
|
+
progress: { phase: 'idle', processed: 0, total: 0, pushed: 0, pulled: 0, skipped: 0, errors: 0 },
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return runControls[profileId];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function makeDeferred() {
|
|
168
|
+
let resolve;
|
|
169
|
+
const promise = new Promise((r) => { resolve = r; });
|
|
170
|
+
return { promise, resolve };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Cooperative checkpoint — call between units of work. Returns:
|
|
174
|
+
// { cancelled: true } if the run should abort, or null to continue.
|
|
175
|
+
async function checkpoint(profileId) {
|
|
176
|
+
const c = getControl(profileId);
|
|
177
|
+
if (c.signal === 'cancel') return { cancelled: true };
|
|
178
|
+
if (c.signal === 'pause') {
|
|
179
|
+
if (!c.resumeDeferred) c.resumeDeferred = makeDeferred();
|
|
180
|
+
// Mirror pause state into persisted status so UI sees it
|
|
181
|
+
try {
|
|
182
|
+
const s = await store().get({ key: STATUS_KEY }) || {};
|
|
183
|
+
s.paused = { ...(s.paused || {}), [profileId]: true };
|
|
184
|
+
await store().set({ key: STATUS_KEY, value: s });
|
|
185
|
+
} catch { /* ignore */ }
|
|
186
|
+
await c.resumeDeferred.promise;
|
|
187
|
+
c.resumeDeferred = null;
|
|
188
|
+
try {
|
|
189
|
+
const s = await store().get({ key: STATUS_KEY }) || {};
|
|
190
|
+
if (s.paused) { delete s.paused[profileId]; }
|
|
191
|
+
await store().set({ key: STATUS_KEY, value: s });
|
|
192
|
+
} catch { /* ignore */ }
|
|
193
|
+
if (c.signal === 'cancel') return { cancelled: true };
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function updateProgress(profileId, patch) {
|
|
199
|
+
const c = getControl(profileId);
|
|
200
|
+
c.progress = { ...c.progress, ...patch };
|
|
201
|
+
try {
|
|
202
|
+
const s = await store().get({ key: STATUS_KEY }) || {};
|
|
203
|
+
s.progress = { ...(s.progress || {}), [profileId]: { ...c.progress } };
|
|
204
|
+
await store().set({ key: STATUS_KEY, value: s });
|
|
205
|
+
} catch { /* ignore */ }
|
|
206
|
+
}
|
|
207
|
+
|
|
150
208
|
function store() {
|
|
151
209
|
return strapi.store({ type: 'plugin', name: PLUGIN_NAME });
|
|
152
210
|
}
|
|
@@ -293,6 +351,9 @@ module.exports = ({ strapi }) => {
|
|
|
293
351
|
lastExecutedAt: p.lastExecutedAt,
|
|
294
352
|
nextExecutionAt: p.nextExecutionAt,
|
|
295
353
|
running: !!(s && s.runningProfiles && s.runningProfiles[p.id]),
|
|
354
|
+
paused: !!(s && s.paused && s.paused[p.id]),
|
|
355
|
+
signal: runControls[p.id]?.signal || 'idle',
|
|
356
|
+
progress: (s && s.progress && s.progress[p.id]) || null,
|
|
296
357
|
})),
|
|
297
358
|
lastRunAt: s?.lastRunAt || null,
|
|
298
359
|
lastResult: s?.lastResult || null,
|
|
@@ -755,13 +816,18 @@ module.exports = ({ strapi }) => {
|
|
|
755
816
|
|
|
756
817
|
/**
|
|
757
818
|
* Copy a page of files respecting settings.batchConcurrency.
|
|
819
|
+
* If checkAbort() returns { cancelled: true }, remaining items are skipped.
|
|
758
820
|
*/
|
|
759
|
-
async function processBatch(items, worker, concurrency) {
|
|
760
|
-
const out = { success: 0, skipped: 0, errors: [] };
|
|
821
|
+
async function processBatch(items, worker, concurrency, checkAbort) {
|
|
822
|
+
const out = { success: 0, skipped: 0, errors: [], cancelled: false };
|
|
761
823
|
const c = Math.max(1, Math.min(concurrency || 1, 10));
|
|
762
824
|
let i = 0;
|
|
763
825
|
async function run() {
|
|
764
826
|
while (i < items.length) {
|
|
827
|
+
if (checkAbort) {
|
|
828
|
+
const abort = await checkAbort();
|
|
829
|
+
if (abort && abort.cancelled) { out.cancelled = true; return; }
|
|
830
|
+
}
|
|
765
831
|
const idx = i++;
|
|
766
832
|
const item = items[idx];
|
|
767
833
|
try {
|
|
@@ -785,15 +851,24 @@ module.exports = ({ strapi }) => {
|
|
|
785
851
|
|
|
786
852
|
const totals = { pushed: 0, pulled: 0, skipped: 0, dbRowsUpdated: 0, morphLinksApplied: 0, morphLinksSkipped: 0, errors: [] };
|
|
787
853
|
const started = Date.now();
|
|
854
|
+
const pid = profile.id;
|
|
855
|
+
const abort = () => checkpoint(pid);
|
|
856
|
+
|
|
857
|
+
await updateProgress(pid, { phase: 'indexing-local', processed: 0, total: 0, pushed: 0, pulled: 0, skipped: 0, errors: 0 });
|
|
788
858
|
|
|
789
859
|
const localIndex = new Map();
|
|
790
860
|
for await (const batch of iterateLocalFiles(settings.pageSize)) {
|
|
861
|
+
const a = await checkpoint(pid);
|
|
862
|
+
if (a && a.cancelled) { totals.cancelled = true; break; }
|
|
791
863
|
for (const f of batch) localIndex.set(`${f.hash}|${f.name}`, f);
|
|
792
864
|
}
|
|
793
865
|
|
|
794
866
|
// PULL: remote -> local
|
|
795
|
-
if (settings.direction === 'pull' || settings.direction === 'both') {
|
|
867
|
+
if (!totals.cancelled && (settings.direction === 'pull' || settings.direction === 'both')) {
|
|
868
|
+
await updateProgress(pid, { phase: 'pull' });
|
|
796
869
|
for await (const remoteBatch of iterateRemoteFiles(remoteConfig, settings.pageSize)) {
|
|
870
|
+
const a = await checkpoint(pid);
|
|
871
|
+
if (a && a.cancelled) { totals.cancelled = true; break; }
|
|
797
872
|
const filtered = remoteBatch.filter((f) => passesFilters(f, profile));
|
|
798
873
|
const result = await processBatch(filtered, async (rf) => {
|
|
799
874
|
const key = `${rf.hash}|${rf.name}`;
|
|
@@ -813,16 +888,24 @@ module.exports = ({ strapi }) => {
|
|
|
813
888
|
await uploadBufferToLocal(rf, buf);
|
|
814
889
|
}
|
|
815
890
|
return 'success';
|
|
816
|
-
}, settings.batchConcurrency);
|
|
891
|
+
}, settings.batchConcurrency, abort);
|
|
817
892
|
totals.pulled += result.success;
|
|
818
893
|
totals.skipped += result.skipped;
|
|
819
894
|
totals.errors.push(...result.errors);
|
|
895
|
+
await updateProgress(pid, {
|
|
896
|
+
pulled: totals.pulled,
|
|
897
|
+
skipped: totals.skipped,
|
|
898
|
+
errors: totals.errors.length,
|
|
899
|
+
processed: totals.pulled + totals.pushed + totals.skipped,
|
|
900
|
+
});
|
|
901
|
+
if (result.cancelled) { totals.cancelled = true; break; }
|
|
820
902
|
}
|
|
821
903
|
}
|
|
822
904
|
|
|
823
|
-
if (profile.syncDbRows) {
|
|
905
|
+
if (!totals.cancelled && profile.syncDbRows) {
|
|
824
906
|
try {
|
|
825
907
|
if (settings.direction === 'pull' || settings.direction === 'both') {
|
|
908
|
+
await updateProgress(pid, { phase: 'pull-morph' });
|
|
826
909
|
const remoteLinks = await fetchRemoteMorphLinks(remoteConfig);
|
|
827
910
|
const applyResult = await applyMorphLinks(remoteLinks);
|
|
828
911
|
totals.morphLinksApplied += applyResult.applied || 0;
|
|
@@ -837,42 +920,58 @@ module.exports = ({ strapi }) => {
|
|
|
837
920
|
}
|
|
838
921
|
|
|
839
922
|
// PUSH: local -> remote
|
|
840
|
-
if (settings.direction === 'push' || settings.direction === 'both') {
|
|
923
|
+
if (!totals.cancelled && (settings.direction === 'push' || settings.direction === 'both')) {
|
|
924
|
+
await updateProgress(pid, { phase: 'push-indexing-remote' });
|
|
841
925
|
const remoteIndex = new Map();
|
|
842
926
|
for await (const remoteBatch of iterateRemoteFiles(remoteConfig, settings.pageSize)) {
|
|
927
|
+
const a = await checkpoint(pid);
|
|
928
|
+
if (a && a.cancelled) { totals.cancelled = true; break; }
|
|
843
929
|
for (const f of remoteBatch) remoteIndex.set(`${f.hash}|${f.name}`, f);
|
|
844
930
|
}
|
|
845
931
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
const
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
const
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
if (
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
932
|
+
if (!totals.cancelled) {
|
|
933
|
+
await updateProgress(pid, { phase: 'push' });
|
|
934
|
+
for await (const localBatch of iterateLocalFiles(settings.pageSize)) {
|
|
935
|
+
const a = await checkpoint(pid);
|
|
936
|
+
if (a && a.cancelled) { totals.cancelled = true; break; }
|
|
937
|
+
const filtered = localBatch.filter((f) => passesFilters(f, profile));
|
|
938
|
+
const result = await processBatch(filtered, async (lf) => {
|
|
939
|
+
const key = `${lf.hash}|${lf.name}`;
|
|
940
|
+
const rf = remoteIndex.get(key);
|
|
941
|
+
|
|
942
|
+
// DB-row sync (push metadata)
|
|
943
|
+
if (profile.syncDbRows && rf) {
|
|
944
|
+
const dbResult = await syncDbRowPush(lf, rf, profile, remoteConfig);
|
|
945
|
+
if (dbResult === 'updated') totals.dbRowsUpdated++;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// File-byte sync
|
|
949
|
+
if (profile.syncFileBytes) {
|
|
950
|
+
if (shouldSkip(lf, rf, settings)) return 'skipped';
|
|
951
|
+
if (settings.dryRun) return 'success';
|
|
952
|
+
const buf = await readLocalFileBuffer(lf);
|
|
953
|
+
if (!buf) return 'skipped';
|
|
954
|
+
await uploadBufferToRemote(remoteConfig, lf, buf);
|
|
955
|
+
}
|
|
956
|
+
return 'success';
|
|
957
|
+
}, settings.batchConcurrency, abort);
|
|
958
|
+
totals.pushed += result.success;
|
|
959
|
+
totals.skipped += result.skipped;
|
|
960
|
+
totals.errors.push(...result.errors);
|
|
961
|
+
await updateProgress(pid, {
|
|
962
|
+
pushed: totals.pushed,
|
|
963
|
+
skipped: totals.skipped,
|
|
964
|
+
errors: totals.errors.length,
|
|
965
|
+
processed: totals.pulled + totals.pushed + totals.skipped,
|
|
966
|
+
});
|
|
967
|
+
if (result.cancelled) { totals.cancelled = true; break; }
|
|
968
|
+
}
|
|
871
969
|
}
|
|
872
970
|
}
|
|
873
971
|
|
|
874
|
-
if (profile.syncDbRows && (settings.direction === 'push' || settings.direction === 'both')) {
|
|
972
|
+
if (!totals.cancelled && profile.syncDbRows && (settings.direction === 'push' || settings.direction === 'both')) {
|
|
875
973
|
try {
|
|
974
|
+
await updateProgress(pid, { phase: 'push-morph' });
|
|
876
975
|
const localLinks = await exportMorphLinks();
|
|
877
976
|
const applyRemoteResult = await applyRemoteMorphLinks(remoteConfig, localLinks);
|
|
878
977
|
totals.morphLinksApplied += applyRemoteResult.applied || 0;
|
|
@@ -885,6 +984,8 @@ module.exports = ({ strapi }) => {
|
|
|
885
984
|
}
|
|
886
985
|
}
|
|
887
986
|
|
|
987
|
+
await updateProgress(pid, { phase: totals.cancelled ? 'cancelled' : 'done' });
|
|
988
|
+
|
|
888
989
|
const summary = {
|
|
889
990
|
strategy: 'url',
|
|
890
991
|
profileId: profile.id,
|
|
@@ -1041,9 +1142,17 @@ module.exports = ({ strapi }) => {
|
|
|
1041
1142
|
const globalSettings = await getGlobalSettings();
|
|
1042
1143
|
const merged = { ...globalSettings, ...profile, ...options };
|
|
1043
1144
|
|
|
1145
|
+
// Reset run controls for a fresh execution
|
|
1146
|
+
const control = getControl(profileId);
|
|
1147
|
+
control.signal = 'run';
|
|
1148
|
+
control.resumeDeferred = null;
|
|
1149
|
+
control.progress = { phase: 'starting', processed: 0, total: 0, pushed: 0, pulled: 0, skipped: 0, errors: 0 };
|
|
1150
|
+
|
|
1044
1151
|
const statusData = await store().get({ key: STATUS_KEY }) || {};
|
|
1045
1152
|
statusData.running = true;
|
|
1046
1153
|
statusData.runningProfiles = { ...(statusData.runningProfiles || {}), [profileId]: true };
|
|
1154
|
+
statusData.progress = { ...(statusData.progress || {}), [profileId]: { ...control.progress } };
|
|
1155
|
+
if (statusData.paused) delete statusData.paused[profileId];
|
|
1047
1156
|
await setStatus(statusData);
|
|
1048
1157
|
|
|
1049
1158
|
try {
|
|
@@ -1059,6 +1168,7 @@ module.exports = ({ strapi }) => {
|
|
|
1059
1168
|
|
|
1060
1169
|
const s2 = await store().get({ key: STATUS_KEY }) || {};
|
|
1061
1170
|
delete (s2.runningProfiles || {})[profileId];
|
|
1171
|
+
if (s2.paused) delete s2.paused[profileId];
|
|
1062
1172
|
s2.running = Object.keys(s2.runningProfiles || {}).length > 0;
|
|
1063
1173
|
s2.lastRunAt = new Date().toISOString();
|
|
1064
1174
|
s2.lastResult = result;
|
|
@@ -1068,6 +1178,7 @@ module.exports = ({ strapi }) => {
|
|
|
1068
1178
|
} catch (err) {
|
|
1069
1179
|
const s2 = await store().get({ key: STATUS_KEY }) || {};
|
|
1070
1180
|
delete (s2.runningProfiles || {})[profileId];
|
|
1181
|
+
if (s2.paused) delete s2.paused[profileId];
|
|
1071
1182
|
s2.running = Object.keys(s2.runningProfiles || {}).length > 0;
|
|
1072
1183
|
s2.lastRunAt = new Date().toISOString();
|
|
1073
1184
|
s2.lastResult = { error: err.message };
|
|
@@ -1076,6 +1187,28 @@ module.exports = ({ strapi }) => {
|
|
|
1076
1187
|
}
|
|
1077
1188
|
}
|
|
1078
1189
|
|
|
1190
|
+
async function pauseProfile(profileId) {
|
|
1191
|
+
const c = getControl(profileId);
|
|
1192
|
+
if (c.signal === 'cancel') return { ok: false, message: 'Run is cancelling' };
|
|
1193
|
+
c.signal = 'pause';
|
|
1194
|
+
return { ok: true, signal: c.signal };
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
async function resumeProfile(profileId) {
|
|
1198
|
+
const c = getControl(profileId);
|
|
1199
|
+
if (c.signal !== 'pause') return { ok: false, message: `Run is not paused (signal=${c.signal})` };
|
|
1200
|
+
c.signal = 'run';
|
|
1201
|
+
if (c.resumeDeferred) { c.resumeDeferred.resolve(); }
|
|
1202
|
+
return { ok: true, signal: c.signal };
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
async function cancelProfile(profileId) {
|
|
1206
|
+
const c = getControl(profileId);
|
|
1207
|
+
c.signal = 'cancel';
|
|
1208
|
+
if (c.resumeDeferred) { c.resumeDeferred.resolve(); } // unblock any pause wait
|
|
1209
|
+
return { ok: true, signal: c.signal };
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1079
1212
|
async function runActiveProfiles() {
|
|
1080
1213
|
const profiles = await getProfiles();
|
|
1081
1214
|
const active = profiles.filter((p) => p.active && p.strategy !== 'disabled');
|
|
@@ -1118,6 +1251,9 @@ module.exports = ({ strapi }) => {
|
|
|
1118
1251
|
// Execution
|
|
1119
1252
|
runProfile,
|
|
1120
1253
|
runActiveProfiles,
|
|
1254
|
+
pauseProfile,
|
|
1255
|
+
resumeProfile,
|
|
1256
|
+
cancelProfile,
|
|
1121
1257
|
|
|
1122
1258
|
// Morph link APIs (documentId-based)
|
|
1123
1259
|
exportMorphLinks,
|
|
@@ -37,6 +37,7 @@ module.exports = ({ strapi }) => {
|
|
|
37
37
|
|
|
38
38
|
const VALID_DIRECTIONS = ['push', 'pull', 'both', 'none'];
|
|
39
39
|
const VALID_CONFLICT_STRATEGIES = ['latest', 'local_wins', 'remote_wins'];
|
|
40
|
+
const VALID_EXECUTION_STRATEGIES = ['hybrid_two_pass', 'one_pass'];
|
|
40
41
|
|
|
41
42
|
async function getSyncMode() {
|
|
42
43
|
const configService = strapi.plugin('strapi-content-sync-pro').service('config');
|
|
@@ -70,24 +71,33 @@ module.exports = ({ strapi }) => {
|
|
|
70
71
|
const data = await store.get({ key: STORE_KEY });
|
|
71
72
|
const profiles = data || [];
|
|
72
73
|
const syncMode = await getSyncMode();
|
|
73
|
-
if (syncMode !== 'single_side') return profiles;
|
|
74
74
|
|
|
75
75
|
let changed = false;
|
|
76
76
|
const normalized = profiles.map((p) => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
:
|
|
89
|
-
|
|
90
|
-
|
|
77
|
+
let next = p;
|
|
78
|
+
|
|
79
|
+
if (!next.executionStrategy) {
|
|
80
|
+
changed = true;
|
|
81
|
+
next = { ...next, executionStrategy: 'hybrid_two_pass' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (syncMode === 'single_side' && next.direction !== 'pull') {
|
|
85
|
+
changed = true;
|
|
86
|
+
next = {
|
|
87
|
+
...next,
|
|
88
|
+
direction: 'pull',
|
|
89
|
+
syncDeletions: !!next.syncDeletions,
|
|
90
|
+
fieldPolicies: Array.isArray(next.fieldPolicies)
|
|
91
|
+
? next.fieldPolicies.map((fp) => ({
|
|
92
|
+
...fp,
|
|
93
|
+
direction: fp.direction === 'none' ? 'none' : 'pull',
|
|
94
|
+
}))
|
|
95
|
+
: next.fieldPolicies,
|
|
96
|
+
updatedAt: new Date().toISOString(),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return next;
|
|
91
101
|
});
|
|
92
102
|
|
|
93
103
|
if (changed) {
|
|
@@ -222,12 +232,17 @@ module.exports = ({ strapi }) => {
|
|
|
222
232
|
}
|
|
223
233
|
}
|
|
224
234
|
|
|
235
|
+
if (profileData.executionStrategy && !VALID_EXECUTION_STRATEGIES.includes(profileData.executionStrategy)) {
|
|
236
|
+
throw new Error(`Invalid execution strategy "${profileData.executionStrategy}"`);
|
|
237
|
+
}
|
|
238
|
+
|
|
225
239
|
const newProfile = {
|
|
226
240
|
id: generateId(),
|
|
227
241
|
name: profileData.name,
|
|
228
242
|
contentType: profileData.contentType,
|
|
229
243
|
direction: profileData.direction || 'both',
|
|
230
244
|
conflictStrategy: profileData.conflictStrategy || 'latest',
|
|
245
|
+
executionStrategy: profileData.executionStrategy || 'hybrid_two_pass',
|
|
231
246
|
syncDeletions: !!profileData.syncDeletions,
|
|
232
247
|
isActive: profileData.isActive || false,
|
|
233
248
|
isSimple: profileData.isSimple !== false, // Default to simple mode
|
|
@@ -299,6 +314,10 @@ module.exports = ({ strapi }) => {
|
|
|
299
314
|
}
|
|
300
315
|
}
|
|
301
316
|
|
|
317
|
+
if (updates.executionStrategy && !VALID_EXECUTION_STRATEGIES.includes(updates.executionStrategy)) {
|
|
318
|
+
throw new Error(`Invalid execution strategy "${updates.executionStrategy}"`);
|
|
319
|
+
}
|
|
320
|
+
|
|
302
321
|
// If setting this profile as active, deactivate others for same content type
|
|
303
322
|
if (updates.isActive) {
|
|
304
323
|
const contentType = updates.contentType || profiles[index].contentType;
|
|
@@ -392,6 +411,7 @@ module.exports = ({ strapi }) => {
|
|
|
392
411
|
return this.createProfile({
|
|
393
412
|
...presetConfig,
|
|
394
413
|
contentType: contentTypeUid,
|
|
414
|
+
executionStrategy: 'hybrid_two_pass',
|
|
395
415
|
syncDeletions: false,
|
|
396
416
|
isSimple: true,
|
|
397
417
|
isActive: false,
|
|
@@ -428,6 +448,7 @@ module.exports = ({ strapi }) => {
|
|
|
428
448
|
return {
|
|
429
449
|
direction: activeProfile.direction,
|
|
430
450
|
conflictStrategy: activeProfile.conflictStrategy,
|
|
451
|
+
executionStrategy: activeProfile.executionStrategy || 'hybrid_two_pass',
|
|
431
452
|
fieldPolicies: activeProfile.isSimple ? null : await this.getFieldPoliciesForContentType(contentTypeUid),
|
|
432
453
|
};
|
|
433
454
|
},
|