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
|
@@ -155,6 +155,31 @@ module.exports = ({ strapi }) => {
|
|
|
155
155
|
return strapi.plugin(PLUGIN_NAME);
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Morph join-table resolution
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// Strapi's upload plugin stores polymorphic file↔entity links in a morph
|
|
162
|
+
// join table whose name differs between Strapi versions (e.g.
|
|
163
|
+
// `files_related_morphs` on some v4 builds vs `files_related_mph` on
|
|
164
|
+
// Strapi v5). Resolve it from ORM metadata so the plugin works everywhere.
|
|
165
|
+
let _morphTableCache = null;
|
|
166
|
+
function resolveMorphTable() {
|
|
167
|
+
if (_morphTableCache) return _morphTableCache;
|
|
168
|
+
const candidates = [];
|
|
169
|
+
try {
|
|
170
|
+
const meta = strapi.db?.metadata?.get?.('plugin::upload.file');
|
|
171
|
+
const attr = meta?.attributes?.related;
|
|
172
|
+
if (attr?.joinTable?.name) candidates.push(attr.joinTable.name);
|
|
173
|
+
if (attr?.pivotTable) candidates.push(attr.pivotTable);
|
|
174
|
+
} catch {
|
|
175
|
+
// ignore — fall back to known names below
|
|
176
|
+
}
|
|
177
|
+
// Known historical defaults, most-recent first.
|
|
178
|
+
candidates.push('files_related_mph', 'files_related_morphs');
|
|
179
|
+
_morphTableCache = candidates.find((n) => !!n) || 'files_related_mph';
|
|
180
|
+
return _morphTableCache;
|
|
181
|
+
}
|
|
182
|
+
|
|
158
183
|
// ---------------------------------------------------------------------------
|
|
159
184
|
// Profile CRUD
|
|
160
185
|
// ---------------------------------------------------------------------------
|
|
@@ -408,6 +433,170 @@ module.exports = ({ strapi }) => {
|
|
|
408
433
|
return 'needs_bytes';
|
|
409
434
|
}
|
|
410
435
|
|
|
436
|
+
async function exportMorphLinks() {
|
|
437
|
+
const morphTable = resolveMorphTable();
|
|
438
|
+
const rows = await strapi.db.connection(morphTable).select('*');
|
|
439
|
+
const fileCache = new Map();
|
|
440
|
+
const relatedDocCache = new Map();
|
|
441
|
+
const out = [];
|
|
442
|
+
|
|
443
|
+
for (const row of rows) {
|
|
444
|
+
const fileId = Number(row.file_id);
|
|
445
|
+
if (!fileCache.has(fileId)) {
|
|
446
|
+
const file = await strapi.db.query('plugin::upload.file').findOne({
|
|
447
|
+
where: { id: fileId },
|
|
448
|
+
select: ['id', 'documentId'],
|
|
449
|
+
});
|
|
450
|
+
fileCache.set(fileId, file || null);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const file = fileCache.get(fileId);
|
|
454
|
+
if (!file?.documentId) continue;
|
|
455
|
+
|
|
456
|
+
const relatedType = row.related_type;
|
|
457
|
+
const relatedId = Number(row.related_id);
|
|
458
|
+
const relatedKey = `${relatedType}:${relatedId}`;
|
|
459
|
+
|
|
460
|
+
if (!relatedDocCache.has(relatedKey)) {
|
|
461
|
+
try {
|
|
462
|
+
const entity = await strapi.db.query(relatedType).findOne({
|
|
463
|
+
where: { id: relatedId },
|
|
464
|
+
select: ['id', 'documentId'],
|
|
465
|
+
});
|
|
466
|
+
relatedDocCache.set(relatedKey, entity?.documentId || null);
|
|
467
|
+
} catch {
|
|
468
|
+
relatedDocCache.set(relatedKey, null);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const relatedDocumentId = relatedDocCache.get(relatedKey);
|
|
473
|
+
if (!relatedDocumentId) continue;
|
|
474
|
+
|
|
475
|
+
out.push({
|
|
476
|
+
fileDocumentId: file.documentId,
|
|
477
|
+
relatedType,
|
|
478
|
+
relatedDocumentId,
|
|
479
|
+
field: row.field || null,
|
|
480
|
+
order: row.order || 1,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return out;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function applyMorphLinks(links = []) {
|
|
488
|
+
const applied = [];
|
|
489
|
+
const skipped = [];
|
|
490
|
+
const errors = [];
|
|
491
|
+
|
|
492
|
+
for (const link of links) {
|
|
493
|
+
try {
|
|
494
|
+
if (!link?.fileDocumentId || !link?.relatedType || !link?.relatedDocumentId) {
|
|
495
|
+
skipped.push({ link, reason: 'missing required documentId fields' });
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const file = await strapi.db.query('plugin::upload.file').findOne({
|
|
500
|
+
where: { documentId: link.fileDocumentId },
|
|
501
|
+
select: ['id', 'documentId'],
|
|
502
|
+
});
|
|
503
|
+
if (!file?.id) {
|
|
504
|
+
skipped.push({ link, reason: 'file documentId not found locally' });
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
let related = null;
|
|
509
|
+
try {
|
|
510
|
+
related = await strapi.db.query(link.relatedType).findOne({
|
|
511
|
+
where: { documentId: link.relatedDocumentId },
|
|
512
|
+
select: ['id', 'documentId'],
|
|
513
|
+
});
|
|
514
|
+
} catch {
|
|
515
|
+
skipped.push({ link, reason: 'related type not queryable locally' });
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (!related?.id) {
|
|
520
|
+
skipped.push({ link, reason: 'related documentId not found locally' });
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const morphTable = resolveMorphTable();
|
|
525
|
+
let existsQ = strapi.db.connection(morphTable)
|
|
526
|
+
.where('file_id', file.id)
|
|
527
|
+
.andWhere('related_id', related.id)
|
|
528
|
+
.andWhere('related_type', link.relatedType);
|
|
529
|
+
|
|
530
|
+
if (link.field) existsQ = existsQ.andWhere('field', link.field);
|
|
531
|
+
else existsQ = existsQ.whereNull('field');
|
|
532
|
+
|
|
533
|
+
const existing = await existsQ.first();
|
|
534
|
+
if (existing) {
|
|
535
|
+
skipped.push({ link, reason: 'morph link already exists' });
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
await strapi.db.connection(morphTable).insert({
|
|
540
|
+
file_id: file.id,
|
|
541
|
+
related_id: related.id,
|
|
542
|
+
related_type: link.relatedType,
|
|
543
|
+
field: link.field || null,
|
|
544
|
+
order: link.order || 1,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
applied.push(link);
|
|
548
|
+
} catch (err) {
|
|
549
|
+
errors.push({ link, error: err.message });
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return {
|
|
554
|
+
total: links.length,
|
|
555
|
+
applied: applied.length,
|
|
556
|
+
skipped: skipped.length,
|
|
557
|
+
errors,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async function fetchRemoteMorphLinks(remoteConfig) {
|
|
562
|
+
const url = new URL('/api/strapi-content-sync-pro/media-sync/morph-links', remoteConfig.baseUrl);
|
|
563
|
+
const res = await fetch(url.toString(), {
|
|
564
|
+
method: 'GET',
|
|
565
|
+
headers: {
|
|
566
|
+
Authorization: `Bearer ${remoteConfig.apiToken}`,
|
|
567
|
+
'Content-Type': 'application/json',
|
|
568
|
+
},
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
if (!res.ok) {
|
|
572
|
+
const body = await safeReadBody(res);
|
|
573
|
+
throw new Error(`Remote morph-links fetch failed (${res.status}): ${body}`);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const json = await res.json();
|
|
577
|
+
return json?.data || [];
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async function applyRemoteMorphLinks(remoteConfig, links = []) {
|
|
581
|
+
const url = new URL('/api/strapi-content-sync-pro/media-sync/morph-links/apply', remoteConfig.baseUrl);
|
|
582
|
+
const res = await fetch(url.toString(), {
|
|
583
|
+
method: 'POST',
|
|
584
|
+
headers: {
|
|
585
|
+
Authorization: `Bearer ${remoteConfig.apiToken}`,
|
|
586
|
+
'Content-Type': 'application/json',
|
|
587
|
+
},
|
|
588
|
+
body: JSON.stringify({ links }),
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
if (!res.ok) {
|
|
592
|
+
const body = await safeReadBody(res);
|
|
593
|
+
throw new Error(`Remote morph-links apply failed (${res.status}): ${body}`);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const json = await res.json();
|
|
597
|
+
return json?.data || { total: links.length, applied: 0, skipped: 0, errors: [] };
|
|
598
|
+
}
|
|
599
|
+
|
|
411
600
|
// ---------------------------------------------------------------------------
|
|
412
601
|
// URL strategy
|
|
413
602
|
// ---------------------------------------------------------------------------
|
|
@@ -594,7 +783,7 @@ module.exports = ({ strapi }) => {
|
|
|
594
783
|
const remoteConfig = await configService.getConfig({ safe: false });
|
|
595
784
|
if (!remoteConfig?.baseUrl) throw new Error('Remote server not configured');
|
|
596
785
|
|
|
597
|
-
const totals = { pushed: 0, pulled: 0, skipped: 0, dbRowsUpdated: 0, errors: [] };
|
|
786
|
+
const totals = { pushed: 0, pulled: 0, skipped: 0, dbRowsUpdated: 0, morphLinksApplied: 0, morphLinksSkipped: 0, errors: [] };
|
|
598
787
|
const started = Date.now();
|
|
599
788
|
|
|
600
789
|
const localIndex = new Map();
|
|
@@ -631,6 +820,22 @@ module.exports = ({ strapi }) => {
|
|
|
631
820
|
}
|
|
632
821
|
}
|
|
633
822
|
|
|
823
|
+
if (profile.syncDbRows) {
|
|
824
|
+
try {
|
|
825
|
+
if (settings.direction === 'pull' || settings.direction === 'both') {
|
|
826
|
+
const remoteLinks = await fetchRemoteMorphLinks(remoteConfig);
|
|
827
|
+
const applyResult = await applyMorphLinks(remoteLinks);
|
|
828
|
+
totals.morphLinksApplied += applyResult.applied || 0;
|
|
829
|
+
totals.morphLinksSkipped += applyResult.skipped || 0;
|
|
830
|
+
if (Array.isArray(applyResult.errors) && applyResult.errors.length > 0) {
|
|
831
|
+
totals.errors.push(...applyResult.errors.map((e) => ({ name: 'morph_pull', error: e.error || 'morph apply error' })));
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
} catch (err) {
|
|
835
|
+
totals.errors.push({ name: 'morph_pull', error: err.message });
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
634
839
|
// PUSH: local -> remote
|
|
635
840
|
if (settings.direction === 'push' || settings.direction === 'both') {
|
|
636
841
|
const remoteIndex = new Map();
|
|
@@ -666,6 +871,20 @@ module.exports = ({ strapi }) => {
|
|
|
666
871
|
}
|
|
667
872
|
}
|
|
668
873
|
|
|
874
|
+
if (profile.syncDbRows && (settings.direction === 'push' || settings.direction === 'both')) {
|
|
875
|
+
try {
|
|
876
|
+
const localLinks = await exportMorphLinks();
|
|
877
|
+
const applyRemoteResult = await applyRemoteMorphLinks(remoteConfig, localLinks);
|
|
878
|
+
totals.morphLinksApplied += applyRemoteResult.applied || 0;
|
|
879
|
+
totals.morphLinksSkipped += applyRemoteResult.skipped || 0;
|
|
880
|
+
if (Array.isArray(applyRemoteResult.errors) && applyRemoteResult.errors.length > 0) {
|
|
881
|
+
totals.errors.push(...applyRemoteResult.errors.map((e) => ({ name: 'morph_push', error: e.error || 'remote morph apply error' })));
|
|
882
|
+
}
|
|
883
|
+
} catch (err) {
|
|
884
|
+
totals.errors.push({ name: 'morph_push', error: err.message });
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
669
888
|
const summary = {
|
|
670
889
|
strategy: 'url',
|
|
671
890
|
profileId: profile.id,
|
|
@@ -900,6 +1119,10 @@ module.exports = ({ strapi }) => {
|
|
|
900
1119
|
runProfile,
|
|
901
1120
|
runActiveProfiles,
|
|
902
1121
|
|
|
1122
|
+
// Morph link APIs (documentId-based)
|
|
1123
|
+
exportMorphLinks,
|
|
1124
|
+
applyMorphLinks,
|
|
1125
|
+
|
|
903
1126
|
// Schedulers
|
|
904
1127
|
initializeSchedulers,
|
|
905
1128
|
stopAllSchedulers,
|
|
@@ -38,6 +38,29 @@ module.exports = ({ strapi }) => {
|
|
|
38
38
|
const VALID_DIRECTIONS = ['push', 'pull', 'both', 'none'];
|
|
39
39
|
const VALID_CONFLICT_STRATEGIES = ['latest', 'local_wins', 'remote_wins'];
|
|
40
40
|
|
|
41
|
+
async function getSyncMode() {
|
|
42
|
+
const configService = strapi.plugin('strapi-content-sync-pro').service('config');
|
|
43
|
+
const config = await configService.getConfig({ safe: false });
|
|
44
|
+
return config?.syncMode || 'paired';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeProfileForMode(profile, syncMode) {
|
|
48
|
+
if (syncMode !== 'single_side') return profile;
|
|
49
|
+
|
|
50
|
+
const next = { ...profile };
|
|
51
|
+
next.direction = 'pull';
|
|
52
|
+
|
|
53
|
+
if (!next.isSimple && Array.isArray(next.fieldPolicies)) {
|
|
54
|
+
next.fieldPolicies = next.fieldPolicies.map((fp) => {
|
|
55
|
+
if (fp.direction === 'push') return { ...fp, direction: 'pull' };
|
|
56
|
+
if (fp.direction === 'both') return { ...fp, direction: 'pull' };
|
|
57
|
+
return fp;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return next;
|
|
62
|
+
}
|
|
63
|
+
|
|
41
64
|
return {
|
|
42
65
|
/**
|
|
43
66
|
* Get all sync profiles
|
|
@@ -45,7 +68,33 @@ module.exports = ({ strapi }) => {
|
|
|
45
68
|
async getProfiles() {
|
|
46
69
|
const store = getStore();
|
|
47
70
|
const data = await store.get({ key: STORE_KEY });
|
|
48
|
-
|
|
71
|
+
const profiles = data || [];
|
|
72
|
+
const syncMode = await getSyncMode();
|
|
73
|
+
if (syncMode !== 'single_side') return profiles;
|
|
74
|
+
|
|
75
|
+
let changed = false;
|
|
76
|
+
const normalized = profiles.map((p) => {
|
|
77
|
+
if (p.direction === 'pull') return p;
|
|
78
|
+
changed = true;
|
|
79
|
+
return {
|
|
80
|
+
...p,
|
|
81
|
+
direction: 'pull',
|
|
82
|
+
syncDeletions: !!p.syncDeletions,
|
|
83
|
+
fieldPolicies: Array.isArray(p.fieldPolicies)
|
|
84
|
+
? p.fieldPolicies.map((fp) => ({
|
|
85
|
+
...fp,
|
|
86
|
+
direction: fp.direction === 'none' ? 'none' : 'pull',
|
|
87
|
+
}))
|
|
88
|
+
: p.fieldPolicies,
|
|
89
|
+
updatedAt: new Date().toISOString(),
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (changed) {
|
|
94
|
+
await store.set({ key: STORE_KEY, value: normalized });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return normalized;
|
|
49
98
|
},
|
|
50
99
|
|
|
51
100
|
/**
|
|
@@ -53,7 +102,10 @@ module.exports = ({ strapi }) => {
|
|
|
53
102
|
*/
|
|
54
103
|
async getProfile(id) {
|
|
55
104
|
const profiles = await this.getProfiles();
|
|
56
|
-
|
|
105
|
+
const profile = profiles.find((p) => p.id === id) || null;
|
|
106
|
+
if (!profile) return null;
|
|
107
|
+
const syncMode = await getSyncMode();
|
|
108
|
+
return normalizeProfileForMode(profile, syncMode);
|
|
57
109
|
},
|
|
58
110
|
|
|
59
111
|
/**
|
|
@@ -61,7 +113,11 @@ module.exports = ({ strapi }) => {
|
|
|
61
113
|
*/
|
|
62
114
|
async getActiveProfileForContentType(contentTypeUid) {
|
|
63
115
|
const profiles = await this.getProfiles();
|
|
64
|
-
|
|
116
|
+
const active = profiles.find((p) => p.contentType === contentTypeUid && p.isActive) || null;
|
|
117
|
+
if (!active) return null;
|
|
118
|
+
|
|
119
|
+
const syncMode = await getSyncMode();
|
|
120
|
+
return normalizeProfileForMode(active, syncMode);
|
|
65
121
|
},
|
|
66
122
|
|
|
67
123
|
/**
|
|
@@ -69,7 +125,10 @@ module.exports = ({ strapi }) => {
|
|
|
69
125
|
*/
|
|
70
126
|
async getProfilesForContentType(contentTypeUid) {
|
|
71
127
|
const profiles = await this.getProfiles();
|
|
72
|
-
|
|
128
|
+
const syncMode = await getSyncMode();
|
|
129
|
+
return profiles
|
|
130
|
+
.filter((p) => p.contentType === contentTypeUid)
|
|
131
|
+
.map((p) => normalizeProfileForMode(p, syncMode));
|
|
73
132
|
},
|
|
74
133
|
|
|
75
134
|
/**
|
|
@@ -91,6 +150,7 @@ module.exports = ({ strapi }) => {
|
|
|
91
150
|
contentType: contentTypeUid,
|
|
92
151
|
direction: 'push',
|
|
93
152
|
conflictStrategy: 'local_wins',
|
|
153
|
+
syncDeletions: false,
|
|
94
154
|
isActive: false,
|
|
95
155
|
isSimple: true,
|
|
96
156
|
fieldPolicies: [],
|
|
@@ -100,6 +160,7 @@ module.exports = ({ strapi }) => {
|
|
|
100
160
|
contentType: contentTypeUid,
|
|
101
161
|
direction: 'pull',
|
|
102
162
|
conflictStrategy: 'remote_wins',
|
|
163
|
+
syncDeletions: false,
|
|
103
164
|
isActive: false,
|
|
104
165
|
isSimple: true,
|
|
105
166
|
fieldPolicies: [],
|
|
@@ -109,6 +170,7 @@ module.exports = ({ strapi }) => {
|
|
|
109
170
|
contentType: contentTypeUid,
|
|
110
171
|
direction: 'both',
|
|
111
172
|
conflictStrategy: 'latest',
|
|
173
|
+
syncDeletions: false,
|
|
112
174
|
isActive: true, // Default active profile
|
|
113
175
|
isSimple: true,
|
|
114
176
|
fieldPolicies: [],
|
|
@@ -166,6 +228,7 @@ module.exports = ({ strapi }) => {
|
|
|
166
228
|
contentType: profileData.contentType,
|
|
167
229
|
direction: profileData.direction || 'both',
|
|
168
230
|
conflictStrategy: profileData.conflictStrategy || 'latest',
|
|
231
|
+
syncDeletions: !!profileData.syncDeletions,
|
|
169
232
|
isActive: profileData.isActive || false,
|
|
170
233
|
isSimple: profileData.isSimple !== false, // Default to simple mode
|
|
171
234
|
fieldPolicies: (profileData.fieldPolicies || []).map((fp) => ({
|
|
@@ -176,6 +239,17 @@ module.exports = ({ strapi }) => {
|
|
|
176
239
|
updatedAt: new Date().toISOString(),
|
|
177
240
|
};
|
|
178
241
|
|
|
242
|
+
const syncMode = await getSyncMode();
|
|
243
|
+
if (syncMode === 'single_side') {
|
|
244
|
+
newProfile.direction = 'pull';
|
|
245
|
+
if (!newProfile.isSimple && Array.isArray(newProfile.fieldPolicies)) {
|
|
246
|
+
newProfile.fieldPolicies = newProfile.fieldPolicies.map((fp) => ({
|
|
247
|
+
...fp,
|
|
248
|
+
direction: fp.direction === 'none' ? 'none' : 'pull',
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
179
253
|
// If this profile is set as active, deactivate others for same content type
|
|
180
254
|
if (newProfile.isActive) {
|
|
181
255
|
profiles.forEach((p) => {
|
|
@@ -238,11 +312,17 @@ module.exports = ({ strapi }) => {
|
|
|
238
312
|
const updatedProfile = {
|
|
239
313
|
...profiles[index],
|
|
240
314
|
...updates,
|
|
315
|
+
syncDeletions: updates.syncDeletions !== undefined ? !!updates.syncDeletions : profiles[index].syncDeletions,
|
|
241
316
|
id: profiles[index].id, // prevent id change
|
|
242
317
|
createdAt: profiles[index].createdAt, // preserve creation date
|
|
243
318
|
updatedAt: new Date().toISOString(),
|
|
244
319
|
};
|
|
245
320
|
|
|
321
|
+
const syncMode = await getSyncMode();
|
|
322
|
+
if (syncMode === 'single_side') {
|
|
323
|
+
updatedProfile.direction = 'pull';
|
|
324
|
+
}
|
|
325
|
+
|
|
246
326
|
if (updates.fieldPolicies) {
|
|
247
327
|
updatedProfile.fieldPolicies = updates.fieldPolicies.map((fp) => ({
|
|
248
328
|
field: fp.field,
|
|
@@ -250,6 +330,13 @@ module.exports = ({ strapi }) => {
|
|
|
250
330
|
}));
|
|
251
331
|
}
|
|
252
332
|
|
|
333
|
+
if (syncMode === 'single_side' && !updatedProfile.isSimple && Array.isArray(updatedProfile.fieldPolicies)) {
|
|
334
|
+
updatedProfile.fieldPolicies = updatedProfile.fieldPolicies.map((fp) => ({
|
|
335
|
+
...fp,
|
|
336
|
+
direction: fp.direction === 'none' ? 'none' : 'pull',
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
|
|
253
340
|
profiles[index] = updatedProfile;
|
|
254
341
|
await store.set({ key: STORE_KEY, value: profiles });
|
|
255
342
|
|
|
@@ -305,6 +392,7 @@ module.exports = ({ strapi }) => {
|
|
|
305
392
|
return this.createProfile({
|
|
306
393
|
...presetConfig,
|
|
307
394
|
contentType: contentTypeUid,
|
|
395
|
+
syncDeletions: false,
|
|
308
396
|
isSimple: true,
|
|
309
397
|
isActive: false,
|
|
310
398
|
fieldPolicies: [],
|