strapi-content-sync-pro 1.0.2 → 1.0.4
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 +67 -18
- package/admin/src/components/BulkTransferTab.jsx +880 -0
- package/admin/src/components/ConfigTab.jsx +25 -4
- package/admin/src/components/HelpTab.jsx +201 -15
- 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 +53 -7
- package/admin/src/pages/App/index.jsx +15 -1
- package/docs/clipchamp-screen-recording-script.md +0 -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 +13 -4
- 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/bulk-transfer.js +141 -0
- package/server/src/controllers/config.js +48 -5
- package/server/src/controllers/index.js +4 -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 +28 -0
- package/server/src/services/bulk-transfer.js +837 -0
- package/server/src/services/config.js +18 -2
- package/server/src/services/index.js +4 -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 +323 -101
- package/server/src/utils/applier.js +120 -13
- package/server/src/utils/comparator.js +22 -6
- package/server/src/utils/fetcher.js +11 -2
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { fetchLocalRecords, fetchRemoteRecords } = require('../utils/fetcher');
|
|
3
|
+
const { fetchLocalRecords, fetchRemoteRecords, fetchLocalPage, fetchRemotePage } = require('../utils/fetcher');
|
|
4
4
|
const { compareRecords } = require('../utils/comparator');
|
|
5
|
-
const { applyLocal, applyRemote } = require('../utils/applier');
|
|
5
|
+
const { applyLocal, applyRemote, deleteLocal, deleteRemote } = require('../utils/applier');
|
|
6
6
|
|
|
7
7
|
const LAST_SYNC_STORE_KEY = 'last-sync-timestamps';
|
|
8
8
|
|
|
@@ -49,115 +49,165 @@ module.exports = ({ strapi }) => {
|
|
|
49
49
|
const syncConfig = await syncConfigService.getSyncConfig();
|
|
50
50
|
const enabledTypes = (syncConfig.contentTypes || []).filter((ct) => ct.enabled);
|
|
51
51
|
|
|
52
|
-
if (enabledTypes.length === 0) {
|
|
53
|
-
throw new Error('No content types configured for sync');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
52
|
// Pagination — remote + local fetches are chunked to keep memory bounded
|
|
57
53
|
// for large datasets. Page size is a global setting tunable in the Sync tab.
|
|
58
54
|
const globalExec = (await executionService.getGlobalSettings?.()) || {};
|
|
59
|
-
const pageSize = Number(globalExec.syncPageSize) || 100;
|
|
60
55
|
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
56
|
+
const syncStatsService = plugin().service('syncStats');
|
|
57
|
+
const reportHandle = await syncStatsService.createRunReport({
|
|
58
|
+
runType: 'sync_now',
|
|
59
|
+
trigger: 'manual_sync_now',
|
|
60
|
+
contentTypes: enabledTypes.map((ct) => ct.uid),
|
|
61
|
+
});
|
|
64
62
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
try {
|
|
64
|
+
if (enabledTypes.length === 0) {
|
|
65
|
+
throw new Error('No content types configured for sync');
|
|
66
|
+
}
|
|
69
67
|
|
|
70
|
-
|
|
71
|
-
const fieldPolicies = await syncProfilesService.getFieldPoliciesForContentType(uid);
|
|
68
|
+
const pageSize = Number(globalExec.syncPageSize) || 100;
|
|
72
69
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
});
|
|
70
|
+
const timestamps = await getLastSyncTimestamps();
|
|
71
|
+
const conflictStrategy = syncConfig.conflictStrategy || 'latest';
|
|
72
|
+
const results = [];
|
|
73
|
+
|
|
74
|
+
for (const ctConfig of enabledTypes) {
|
|
75
|
+
const { uid, direction, fields } = ctConfig;
|
|
76
|
+
const lastSyncAt = timestamps[uid] || null;
|
|
77
|
+
const syncStartTime = new Date().toISOString();
|
|
78
|
+
|
|
79
|
+
// Get field-level policies from active profile (if any)
|
|
80
|
+
const fieldPolicies = await syncProfilesService.getFieldPoliciesForContentType(uid);
|
|
85
81
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
82
|
+
try {
|
|
83
|
+
// Both sides are fetched in pages of `pageSize` records under the
|
|
84
|
+
// hood (see utils/fetcher.js). We aggregate per content-type because
|
|
85
|
+
// the comparator needs the full set to diff by syncId, but each
|
|
86
|
+
// network/DB call still only returns a bounded chunk.
|
|
87
|
+
const localRecords = await fetchLocalRecords(strapi, uid, { fields, lastSyncAt, pageSize });
|
|
88
|
+
const remoteRecords = await fetchRemoteRecords(remoteConfig, uid, { fields, lastSyncAt, pageSize });
|
|
89
|
+
|
|
90
|
+
const profileForOptions = await syncProfilesService.getActiveProfileForContentType(uid);
|
|
91
|
+
const syncDeletions = !!(profileForOptions?.syncDeletions);
|
|
92
|
+
|
|
93
|
+
const diff = compareRecords(localRecords, remoteRecords, {
|
|
94
|
+
direction,
|
|
95
|
+
conflictStrategy,
|
|
96
|
+
syncDeletions,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
let pushed = 0;
|
|
100
|
+
let pulled = 0;
|
|
101
|
+
let errors = 0;
|
|
102
|
+
|
|
103
|
+
// Apply field policies to records before pushing/pulling
|
|
104
|
+
for (const { local } of diff.toPush) {
|
|
105
|
+
try {
|
|
106
|
+
const filteredRecord = syncProfilesService.filterFieldsByPolicy(local, fieldPolicies, 'push');
|
|
107
|
+
await applyRemote(remoteConfig, uid, filteredRecord, fields);
|
|
108
|
+
pushed++;
|
|
109
|
+
} catch (err) {
|
|
110
|
+
errors++;
|
|
111
|
+
await logService.log({ action: 'push', contentType: uid, syncId: local.syncId, direction: 'push', status: 'error', message: err.message });
|
|
112
|
+
}
|
|
99
113
|
}
|
|
100
|
-
}
|
|
101
114
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
115
|
+
for (const { remote } of diff.toPull) {
|
|
116
|
+
try {
|
|
117
|
+
const filteredRecord = syncProfilesService.filterFieldsByPolicy(remote, fieldPolicies, 'pull');
|
|
118
|
+
await applyLocal(strapi, uid, filteredRecord, fields);
|
|
119
|
+
pulled++;
|
|
120
|
+
} catch (err) {
|
|
121
|
+
errors++;
|
|
122
|
+
await logService.log({ action: 'pull', contentType: uid, syncId: remote.syncId, direction: 'pull', status: 'error', message: err.message });
|
|
123
|
+
}
|
|
110
124
|
}
|
|
111
|
-
}
|
|
112
125
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
126
|
+
for (const record of diff.toCreateRemote) {
|
|
127
|
+
try {
|
|
128
|
+
const filteredRecord = syncProfilesService.filterFieldsByPolicy(record, fieldPolicies, 'push');
|
|
129
|
+
await applyRemote(remoteConfig, uid, filteredRecord, fields);
|
|
130
|
+
pushed++;
|
|
131
|
+
} catch (err) {
|
|
132
|
+
errors++;
|
|
133
|
+
await logService.log({ action: 'create_remote', contentType: uid, syncId: record.syncId, direction: 'push', status: 'error', message: err.message });
|
|
134
|
+
}
|
|
121
135
|
}
|
|
122
|
-
}
|
|
123
136
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
137
|
+
for (const record of diff.toCreateLocal) {
|
|
138
|
+
try {
|
|
139
|
+
const filteredRecord = syncProfilesService.filterFieldsByPolicy(record, fieldPolicies, 'pull');
|
|
140
|
+
await applyLocal(strapi, uid, filteredRecord, fields);
|
|
141
|
+
pulled++;
|
|
142
|
+
} catch (err) {
|
|
143
|
+
errors++;
|
|
144
|
+
await logService.log({ action: 'create_local', contentType: uid, syncId: record.syncId, direction: 'pull', status: 'error', message: err.message });
|
|
145
|
+
}
|
|
132
146
|
}
|
|
133
|
-
}
|
|
134
147
|
|
|
135
|
-
|
|
148
|
+
for (const record of diff.toDeleteRemote) {
|
|
149
|
+
try {
|
|
150
|
+
await deleteRemote(remoteConfig, uid, record);
|
|
151
|
+
await logService.log({ action: 'delete_remote', contentType: uid, syncId: record.syncId, direction: 'push', status: 'success', message: `Deleted remote record ${record.syncId}` });
|
|
152
|
+
} catch (err) {
|
|
153
|
+
errors++;
|
|
154
|
+
await logService.log({ action: 'delete_remote', contentType: uid, syncId: record.syncId, direction: 'push', status: 'error', message: err.message });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
136
157
|
|
|
137
|
-
|
|
138
|
-
|
|
158
|
+
for (const record of diff.toDeleteLocal) {
|
|
159
|
+
try {
|
|
160
|
+
await deleteLocal(strapi, uid, record);
|
|
161
|
+
await logService.log({ action: 'delete_local', contentType: uid, syncId: record.syncId, direction: 'pull', status: 'success', message: `Deleted local record ${record.syncId}` });
|
|
162
|
+
} catch (err) {
|
|
163
|
+
errors++;
|
|
164
|
+
await logService.log({ action: 'delete_local', contentType: uid, syncId: record.syncId, direction: 'pull', status: 'error', message: err.message });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
139
167
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
168
|
+
await setLastSyncTimestamp(uid, syncStartTime);
|
|
169
|
+
|
|
170
|
+
const summary = { uid, pushed, pulled, errors, hasFieldPolicies: !!fieldPolicies };
|
|
171
|
+
results.push(summary);
|
|
172
|
+
|
|
173
|
+
await logService.log({
|
|
174
|
+
action: 'sync_complete',
|
|
175
|
+
contentType: uid,
|
|
176
|
+
direction,
|
|
177
|
+
status: errors > 0 ? 'partial' : 'success',
|
|
178
|
+
message: `Pushed: ${pushed}, Pulled: ${pulled}, Errors: ${errors}${fieldPolicies ? ' (with field policies)' : ''}`,
|
|
179
|
+
details: summary,
|
|
180
|
+
});
|
|
181
|
+
} catch (err) {
|
|
182
|
+
results.push({ uid, error: err.message });
|
|
183
|
+
await logService.log({
|
|
184
|
+
action: 'sync_error',
|
|
185
|
+
contentType: uid,
|
|
186
|
+
direction,
|
|
187
|
+
status: 'error',
|
|
188
|
+
message: err.message,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
157
191
|
}
|
|
158
|
-
}
|
|
159
192
|
|
|
160
|
-
|
|
193
|
+
const response = { syncedAt: new Date().toISOString(), results };
|
|
194
|
+
await syncStatsService.completeRunReport(reportHandle.reportId, {
|
|
195
|
+
status: 'success',
|
|
196
|
+
summary: response,
|
|
197
|
+
});
|
|
198
|
+
await plugin().service('syncLog').applyRetention({ maxLogs: globalExec.maxLogEntries });
|
|
199
|
+
await syncStatsService.applyRetention({ maxReports: globalExec.maxReportEntries });
|
|
200
|
+
return response;
|
|
201
|
+
} catch (err) {
|
|
202
|
+
await syncStatsService.completeRunReport(reportHandle.reportId, {
|
|
203
|
+
status: 'error',
|
|
204
|
+
summary: null,
|
|
205
|
+
error: err.message,
|
|
206
|
+
});
|
|
207
|
+
await plugin().service('syncLog').applyRetention({ maxLogs: globalExec.maxLogEntries });
|
|
208
|
+
await syncStatsService.applyRetention({ maxReports: globalExec.maxReportEntries });
|
|
209
|
+
throw err;
|
|
210
|
+
}
|
|
161
211
|
},
|
|
162
212
|
|
|
163
213
|
/**
|
|
@@ -191,6 +241,7 @@ module.exports = ({ strapi }) => {
|
|
|
191
241
|
|
|
192
242
|
const direction = profile?.direction || ctConfig.direction || 'both';
|
|
193
243
|
const conflictStrategy = profile?.conflictStrategy || syncConfig.conflictStrategy || 'latest';
|
|
244
|
+
const syncDeletions = !!(profile?.syncDeletions);
|
|
194
245
|
const fields = ctConfig.fields || [];
|
|
195
246
|
|
|
196
247
|
// Field-level policies: prefer the policies on the provided profile,
|
|
@@ -222,7 +273,7 @@ module.exports = ({ strapi }) => {
|
|
|
222
273
|
const localRecords = await fetchLocalRecords(strapi, uid, { fields, lastSyncAt, pageSize });
|
|
223
274
|
const remoteRecords = await fetchRemoteRecords(remoteConfig, uid, { fields, lastSyncAt, pageSize });
|
|
224
275
|
|
|
225
|
-
const diff = compareRecords(localRecords, remoteRecords, { direction, conflictStrategy });
|
|
276
|
+
const diff = compareRecords(localRecords, remoteRecords, { direction, conflictStrategy, syncDeletions });
|
|
226
277
|
|
|
227
278
|
for (const { local } of diff.toPush) {
|
|
228
279
|
try {
|
|
@@ -268,6 +319,26 @@ module.exports = ({ strapi }) => {
|
|
|
268
319
|
}
|
|
269
320
|
}
|
|
270
321
|
|
|
322
|
+
for (const record of diff.toDeleteRemote) {
|
|
323
|
+
try {
|
|
324
|
+
await deleteRemote(remoteConfig, uid, record);
|
|
325
|
+
await logService.log({ action: 'delete_remote', contentType: uid, syncId: record.syncId, direction: 'push', status: 'success', message: `Deleted remote record ${record.syncId}` });
|
|
326
|
+
} catch (err) {
|
|
327
|
+
errors++;
|
|
328
|
+
await logService.log({ action: 'delete_remote', contentType: uid, syncId: record.syncId, direction: 'push', status: 'error', message: err.message });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
for (const record of diff.toDeleteLocal) {
|
|
333
|
+
try {
|
|
334
|
+
await deleteLocal(strapi, uid, record);
|
|
335
|
+
await logService.log({ action: 'delete_local', contentType: uid, syncId: record.syncId, direction: 'pull', status: 'success', message: `Deleted local record ${record.syncId}` });
|
|
336
|
+
} catch (err) {
|
|
337
|
+
errors++;
|
|
338
|
+
await logService.log({ action: 'delete_local', contentType: uid, syncId: record.syncId, direction: 'pull', status: 'error', message: err.message });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
271
342
|
await setLastSyncTimestamp(uid, syncStartTime);
|
|
272
343
|
|
|
273
344
|
const summary = {
|
|
@@ -301,6 +372,142 @@ module.exports = ({ strapi }) => {
|
|
|
301
372
|
}
|
|
302
373
|
},
|
|
303
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Sync a SINGLE PAGE of a content type. Used by the bulk-transfer (Full
|
|
377
|
+
* Sync) engine to process large content types page-by-page so that
|
|
378
|
+
* progress can be reported and the job can be paused / resumed between
|
|
379
|
+
* pages.
|
|
380
|
+
*
|
|
381
|
+
* options:
|
|
382
|
+
* - profile: synthetic/real profile (direction, conflictStrategy, syncDeletions)
|
|
383
|
+
* - page: 1-based page number (default 1)
|
|
384
|
+
* - pageSize: records per page (default from global settings or 100)
|
|
385
|
+
* - lastSyncAt: optional ISO timestamp; when omitted this runs a full
|
|
386
|
+
* page scan (preferred for bulk transfer). When provided it acts
|
|
387
|
+
* incremental.
|
|
388
|
+
*
|
|
389
|
+
* Returns:
|
|
390
|
+
* { uid, page, pageSize, pushed, pulled, errors, hasMore,
|
|
391
|
+
* localCount, remoteCount, remoteTotal, remotePageCount }
|
|
392
|
+
*/
|
|
393
|
+
async syncContentTypePage(uid, options = {}) {
|
|
394
|
+
if (!uid) throw new Error('Content type uid is required');
|
|
395
|
+
|
|
396
|
+
const logService = plugin().service('syncLog');
|
|
397
|
+
const configService = plugin().service('config');
|
|
398
|
+
const syncConfigService = plugin().service('syncConfig');
|
|
399
|
+
const syncProfilesService = plugin().service('syncProfiles');
|
|
400
|
+
const executionService = plugin().service('syncExecution');
|
|
401
|
+
|
|
402
|
+
const remoteConfig = await configService.getConfig({ safe: false });
|
|
403
|
+
if (!remoteConfig || !remoteConfig.baseUrl) {
|
|
404
|
+
throw new Error('Remote server not configured');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const { profile } = options;
|
|
408
|
+
const syncConfig = await syncConfigService.getSyncConfig();
|
|
409
|
+
const ctConfig = (syncConfig.contentTypes || []).find((ct) => ct.uid === uid) || { uid, fields: [] };
|
|
410
|
+
|
|
411
|
+
const direction = profile?.direction || ctConfig.direction || 'both';
|
|
412
|
+
const conflictStrategy = profile?.conflictStrategy || syncConfig.conflictStrategy || 'latest';
|
|
413
|
+
const syncDeletions = !!(profile?.syncDeletions);
|
|
414
|
+
const fields = ctConfig.fields || [];
|
|
415
|
+
|
|
416
|
+
let fieldPolicies = null;
|
|
417
|
+
if (profile && !profile.isSimple && Array.isArray(profile.fieldPolicies) && profile.fieldPolicies.length > 0) {
|
|
418
|
+
fieldPolicies = {};
|
|
419
|
+
for (const fp of profile.fieldPolicies) fieldPolicies[fp.field] = fp.direction;
|
|
420
|
+
} else if (!profile) {
|
|
421
|
+
fieldPolicies = await syncProfilesService.getFieldPoliciesForContentType(uid);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const globalExec = (await executionService.getGlobalSettings?.()) || {};
|
|
425
|
+
const pageSize = Number(options.pageSize) || Number(globalExec.syncPageSize) || 100;
|
|
426
|
+
const page = Math.max(1, Number(options.page) || 1);
|
|
427
|
+
const lastSyncAt = options.lastSyncAt || null;
|
|
428
|
+
|
|
429
|
+
let pushed = 0;
|
|
430
|
+
let pulled = 0;
|
|
431
|
+
let errors = 0;
|
|
432
|
+
|
|
433
|
+
const localPageRes = await fetchLocalPage(strapi, uid, { fields, lastSyncAt, page, pageSize });
|
|
434
|
+
const remotePageRes = await fetchRemotePage(remoteConfig, uid, { fields, lastSyncAt, page, pageSize });
|
|
435
|
+
|
|
436
|
+
const localRecords = localPageRes.records || [];
|
|
437
|
+
const remoteRecords = remotePageRes.records || [];
|
|
438
|
+
|
|
439
|
+
// NOTE: comparator works on the page slice only. Cross-side deletion
|
|
440
|
+
// detection is intentionally disabled here because a record missing
|
|
441
|
+
// from this page may live on another page; full-set deletion sync
|
|
442
|
+
// should use the incremental path instead.
|
|
443
|
+
const diff = compareRecords(localRecords, remoteRecords, {
|
|
444
|
+
direction,
|
|
445
|
+
conflictStrategy,
|
|
446
|
+
syncDeletions: false,
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
for (const { local } of diff.toPush) {
|
|
450
|
+
try {
|
|
451
|
+
const filtered = syncProfilesService.filterFieldsByPolicy(local, fieldPolicies, 'push');
|
|
452
|
+
await applyRemote(remoteConfig, uid, filtered, fields);
|
|
453
|
+
pushed++;
|
|
454
|
+
} catch (err) {
|
|
455
|
+
errors++;
|
|
456
|
+
await logService.log({ action: 'push', contentType: uid, syncId: local.syncId, direction: 'push', status: 'error', message: err.message });
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
for (const { remote } of diff.toPull) {
|
|
461
|
+
try {
|
|
462
|
+
const filtered = syncProfilesService.filterFieldsByPolicy(remote, fieldPolicies, 'pull');
|
|
463
|
+
await applyLocal(strapi, uid, filtered, fields);
|
|
464
|
+
pulled++;
|
|
465
|
+
} catch (err) {
|
|
466
|
+
errors++;
|
|
467
|
+
await logService.log({ action: 'pull', contentType: uid, syncId: remote.syncId, direction: 'pull', status: 'error', message: err.message });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
for (const record of diff.toCreateRemote) {
|
|
472
|
+
try {
|
|
473
|
+
const filtered = syncProfilesService.filterFieldsByPolicy(record, fieldPolicies, 'push');
|
|
474
|
+
await applyRemote(remoteConfig, uid, filtered, fields);
|
|
475
|
+
pushed++;
|
|
476
|
+
} catch (err) {
|
|
477
|
+
errors++;
|
|
478
|
+
await logService.log({ action: 'create_remote', contentType: uid, syncId: record.syncId, direction: 'push', status: 'error', message: err.message });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
for (const record of diff.toCreateLocal) {
|
|
483
|
+
try {
|
|
484
|
+
const filtered = syncProfilesService.filterFieldsByPolicy(record, fieldPolicies, 'pull');
|
|
485
|
+
await applyLocal(strapi, uid, filtered, fields);
|
|
486
|
+
pulled++;
|
|
487
|
+
} catch (err) {
|
|
488
|
+
errors++;
|
|
489
|
+
await logService.log({ action: 'create_local', contentType: uid, syncId: record.syncId, direction: 'pull', status: 'error', message: err.message });
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// hasMore is the OR of both sides so we keep paging until both are drained
|
|
494
|
+
const hasMore = !!(localPageRes.hasMore || remotePageRes.hasMore);
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
uid,
|
|
498
|
+
page,
|
|
499
|
+
pageSize,
|
|
500
|
+
pushed,
|
|
501
|
+
pulled,
|
|
502
|
+
errors,
|
|
503
|
+
hasMore,
|
|
504
|
+
localCount: localRecords.length,
|
|
505
|
+
remoteCount: remoteRecords.length,
|
|
506
|
+
remoteTotal: remotePageRes.total,
|
|
507
|
+
remotePageCount: remotePageRes.pageCount,
|
|
508
|
+
};
|
|
509
|
+
},
|
|
510
|
+
|
|
304
511
|
/**
|
|
305
512
|
* Step 8 — Push a single record to the remote (called by lifecycle hooks).
|
|
306
513
|
* Now supports field-level policies.
|
|
@@ -352,32 +559,47 @@ module.exports = ({ strapi }) => {
|
|
|
352
559
|
* Step 9 — Receive a record pushed from a remote instance.
|
|
353
560
|
* Now supports field-level policies.
|
|
354
561
|
*/
|
|
355
|
-
async receiveRecord(uid, data, syncId) {
|
|
562
|
+
async receiveRecord(uid, data, syncId, isDelete = false, documentId = null) {
|
|
356
563
|
const logService = plugin().service('syncLog');
|
|
357
564
|
const syncProfilesService = plugin().service('syncProfiles');
|
|
358
565
|
|
|
359
|
-
|
|
360
|
-
const fieldPolicies = await syncProfilesService.getFieldPoliciesForContentType(uid);
|
|
361
|
-
const filteredData = syncProfilesService.filterFieldsByPolicy(data, fieldPolicies, 'pull');
|
|
566
|
+
const key = documentId || syncId;
|
|
362
567
|
|
|
363
568
|
try {
|
|
364
|
-
|
|
569
|
+
if (isDelete) {
|
|
570
|
+
await deleteLocal(strapi, uid, { documentId, syncId });
|
|
571
|
+
await logService.log({
|
|
572
|
+
action: 'receive_delete',
|
|
573
|
+
contentType: uid,
|
|
574
|
+
syncId: key,
|
|
575
|
+
direction: 'pull',
|
|
576
|
+
status: 'success',
|
|
577
|
+
message: `Delete received for ${key} from remote`,
|
|
578
|
+
});
|
|
579
|
+
return { success: true, deleted: true };
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Get field-level policies from active profile (if any)
|
|
583
|
+
const fieldPolicies = await syncProfilesService.getFieldPoliciesForContentType(uid);
|
|
584
|
+
const filteredData = syncProfilesService.filterFieldsByPolicy(data, fieldPolicies, 'pull');
|
|
585
|
+
|
|
586
|
+
await applyLocal(strapi, uid, { ...filteredData, documentId, syncId }, []);
|
|
365
587
|
|
|
366
588
|
await logService.log({
|
|
367
589
|
action: 'receive',
|
|
368
590
|
contentType: uid,
|
|
369
|
-
syncId,
|
|
591
|
+
syncId: key,
|
|
370
592
|
direction: 'pull',
|
|
371
593
|
status: 'success',
|
|
372
|
-
message: `Record ${
|
|
594
|
+
message: `Record ${key} received from remote${fieldPolicies ? ' (with field policies)' : ''}`,
|
|
373
595
|
});
|
|
374
596
|
|
|
375
597
|
return { success: true };
|
|
376
598
|
} catch (err) {
|
|
377
599
|
await logService.log({
|
|
378
|
-
action: 'receive',
|
|
600
|
+
action: isDelete ? 'receive_delete' : 'receive',
|
|
379
601
|
contentType: uid,
|
|
380
|
-
syncId,
|
|
602
|
+
syncId: key,
|
|
381
603
|
direction: 'pull',
|
|
382
604
|
status: 'error',
|
|
383
605
|
message: err.message,
|