strapi-content-sync-pro 1.0.0

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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +206 -0
  3. package/admin/src/components/ConfigTab.jsx +1038 -0
  4. package/admin/src/components/ContentTypesTab.jsx +160 -0
  5. package/admin/src/components/HelpTab.jsx +945 -0
  6. package/admin/src/components/LogsTab.jsx +136 -0
  7. package/admin/src/components/MediaTab.jsx +557 -0
  8. package/admin/src/components/SyncProfilesTab.jsx +715 -0
  9. package/admin/src/components/SyncTab.jsx +988 -0
  10. package/admin/src/index.js +31 -0
  11. package/admin/src/pages/App/index.jsx +129 -0
  12. package/admin/src/pluginId.js +3 -0
  13. package/package.json +84 -0
  14. package/server/src/bootstrap.js +151 -0
  15. package/server/src/config/index.js +5 -0
  16. package/server/src/content-types/index.js +7 -0
  17. package/server/src/content-types/sync-log/schema.json +24 -0
  18. package/server/src/controllers/alerts.js +59 -0
  19. package/server/src/controllers/config.js +292 -0
  20. package/server/src/controllers/content-type-discovery.js +9 -0
  21. package/server/src/controllers/dependencies.js +109 -0
  22. package/server/src/controllers/index.js +29 -0
  23. package/server/src/controllers/ping.js +7 -0
  24. package/server/src/controllers/sync-config.js +26 -0
  25. package/server/src/controllers/sync-enforcement.js +323 -0
  26. package/server/src/controllers/sync-execution.js +134 -0
  27. package/server/src/controllers/sync-log.js +18 -0
  28. package/server/src/controllers/sync-media.js +158 -0
  29. package/server/src/controllers/sync-profiles.js +182 -0
  30. package/server/src/controllers/sync.js +31 -0
  31. package/server/src/destroy.js +7 -0
  32. package/server/src/index.js +21 -0
  33. package/server/src/middlewares/verify-signature.js +32 -0
  34. package/server/src/register.js +7 -0
  35. package/server/src/routes/index.js +111 -0
  36. package/server/src/services/alerts.js +437 -0
  37. package/server/src/services/config.js +68 -0
  38. package/server/src/services/content-type-discovery.js +41 -0
  39. package/server/src/services/dependency-resolver.js +284 -0
  40. package/server/src/services/index.js +30 -0
  41. package/server/src/services/ping.js +7 -0
  42. package/server/src/services/sync-config.js +45 -0
  43. package/server/src/services/sync-enforcement.js +362 -0
  44. package/server/src/services/sync-execution.js +541 -0
  45. package/server/src/services/sync-log.js +56 -0
  46. package/server/src/services/sync-media.js +963 -0
  47. package/server/src/services/sync-profiles.js +380 -0
  48. package/server/src/services/sync.js +248 -0
  49. package/server/src/utils/applier.js +89 -0
  50. package/server/src/utils/comparator.js +83 -0
  51. package/server/src/utils/fetcher.js +142 -0
  52. package/server/src/utils/hmac.js +37 -0
  53. package/server/src/utils/pagination.js +51 -0
  54. package/server/src/utils/sync-guard.js +29 -0
  55. package/server/src/utils/sync-id.js +16 -0
@@ -0,0 +1,31 @@
1
+ import { ArrowsCounterClockwise } from '@strapi/icons';
2
+ import pluginId from './pluginId';
3
+
4
+ export default {
5
+ register(app) {
6
+ app.addMenuLink({
7
+ to: `plugins/${pluginId}`,
8
+ icon: ArrowsCounterClockwise,
9
+ intlLabel: {
10
+ id: `${pluginId}.plugin.name`,
11
+ defaultMessage: 'Data Sync',
12
+ },
13
+ Component: async () => {
14
+ const { App } = await import('./pages/App');
15
+ return App;
16
+ },
17
+ });
18
+
19
+ app.registerPlugin({
20
+ id: pluginId,
21
+ name: pluginId,
22
+ });
23
+ },
24
+
25
+ async registerTrads({ locales }) {
26
+ return locales.map((locale) => ({
27
+ data: {},
28
+ locale,
29
+ }));
30
+ },
31
+ };
@@ -0,0 +1,129 @@
1
+ import { useState } from 'react';
2
+ import { Routes, Route } from 'react-router-dom';
3
+ import { Page } from '@strapi/strapi/admin';
4
+ import { Box, Flex, Typography, Button, Main } from '@strapi/design-system';
5
+ import { ConfigTab } from '../../components/ConfigTab';
6
+ import { ContentTypesTab } from '../../components/ContentTypesTab';
7
+ import { SyncTab } from '../../components/SyncTab';
8
+ import { LogsTab } from '../../components/LogsTab';
9
+ import { HelpTab } from '../../components/HelpTab';
10
+ import { SyncProfilesTab } from '../../components/SyncProfilesTab';
11
+ import { MediaTab } from '../../components/MediaTab';
12
+
13
+ const TABS = [
14
+ { key: 'config', label: 'Configuration' },
15
+ { key: 'content-types', label: 'Content Types' },
16
+ { key: 'sync-profiles', label: 'Sync Profiles' },
17
+ { key: 'sync', label: 'Sync' },
18
+ { key: 'media', label: 'Media' },
19
+ { key: 'logs', label: 'Logs' },
20
+ { key: 'help', label: 'Help' },
21
+ ];
22
+
23
+ const HomePage = () => {
24
+ const [activeTab, setActiveTab] = useState('config');
25
+
26
+ return (
27
+ <Main>
28
+ <Box padding={8} background="neutral100">
29
+ <Typography variant="alpha" tag="h1">
30
+ Content Sync Pro Plugin - Reliable Data Synchronization for Strapi Environments
31
+ </Typography>
32
+
33
+
34
+ <Box paddingTop={4} paddingBottom={6}>
35
+ <Flex gap={2}>
36
+ {TABS.map((tab) => (
37
+ <Button
38
+ key={tab.key}
39
+ variant={activeTab === tab.key ? 'default' : 'tertiary'}
40
+ onClick={() => setActiveTab(tab.key)}
41
+ >
42
+ {tab.label}
43
+ </Button>
44
+ ))}
45
+ </Flex>
46
+ </Box>
47
+
48
+ {activeTab === 'config' && <ConfigTab />}
49
+ {activeTab === 'content-types' && <ContentTypesTab />}
50
+ {activeTab === 'sync-profiles' && <SyncProfilesTab />}
51
+ {activeTab === 'sync' && <SyncTab />}
52
+ {activeTab === 'media' && <MediaTab />}
53
+ {activeTab === 'logs' && <LogsTab />}
54
+ {activeTab === 'help' && <HelpTab />}
55
+
56
+ <Box paddingTop={8} borderColor="neutral200" borderStyle="solid" borderWidth="1px 0 0 0">
57
+ <Box paddingTop={4}>
58
+ <Typography variant="sigma" textColor="neutral600">
59
+ Need help with this plugin? Feel free to reach out or explore the resources below.
60
+ </Typography>
61
+ <Box paddingTop={2}>
62
+ <Typography variant="pi" textColor="neutral500">
63
+ Contact: {' Eja Arain '}
64
+ <Typography
65
+ variant="pi"
66
+ textColor="primary600"
67
+ tag="a"
68
+ href="mailto:eharain@yahoo.com"
69
+ >
70
+ eharain@yahoo.com
71
+ </Typography>
72
+ {' · '}
73
+ <Typography
74
+ variant="pi"
75
+ textColor="primary600"
76
+ tag="a"
77
+ href="https://github.com/eharain/strapi-content-sync-pro"
78
+ target="_blank"
79
+ rel="noopener noreferrer"
80
+ >
81
+ GitHub
82
+ </Typography>
83
+ {' · '}
84
+ <Typography
85
+ variant="pi"
86
+ textColor="primary600"
87
+ tag="a"
88
+ href="https://www.linkedin.com/in/ejazarain/"
89
+ target="_blank"
90
+ rel="noopener noreferrer"
91
+ >
92
+ LinkedIn
93
+ </Typography>
94
+ </Typography>
95
+ </Box>
96
+ <Box paddingTop={2}>
97
+ <Typography variant="pi" textColor="neutral500">
98
+ This plugin is a core component of {' '}
99
+ <Typography
100
+ variant="pi"
101
+ textColor="primary600"
102
+ tag="a"
103
+ href="https://github.com/eharain/Rutba-ERP/"
104
+ target="_blank"
105
+ rel="noopener noreferrer"
106
+ >
107
+ Rutba ERP
108
+ </Typography>
109
+ , powering reliable data synchronization across multi-environment installations.
110
+ </Typography>
111
+ </Box>
112
+ </Box>
113
+ </Box>
114
+ </Box>
115
+ </Main>
116
+ );
117
+ };
118
+
119
+ const App = () => {
120
+ return (
121
+ <Routes>
122
+ <Route index element={<HomePage />} />
123
+ <Route path="*" element={<Page.Error />} />
124
+ </Routes>
125
+ );
126
+ };
127
+
128
+ export { App };
129
+ export default App;
@@ -0,0 +1,3 @@
1
+ const pluginId = 'strapi-content-sync-pro';
2
+
3
+ export default pluginId;
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "strapi-content-sync-pro",
3
+ "version": "1.0.0",
4
+ "description": "Strapi v5 plugin to copy, migrate, and live-sync content, media, and data between multiple Strapi environments with bi-directional sync, field-level policies, scheduling, and alerts.",
5
+ "license": "MIT",
6
+ "author": {
7
+ "name": "Ejaz Husain Arain",
8
+ "email": "eharain@yahoo.com",
9
+ "url": "https://github.com/eharain"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/eharain/strapi-content-sync-pro.git"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/eharain/strapi-content-sync-pro/issues"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public",
20
+ "registry": "https://registry.npmjs.org/"
21
+ },
22
+ "homepage": "https://github.com/eharain/strapi-content-sync-pro#readme",
23
+ "keywords": [
24
+ "strapi",
25
+ "strapi-plugin",
26
+ "strapi-v5",
27
+ "sync",
28
+ "live-sync",
29
+ "data-sync",
30
+ "content-sync",
31
+ "copy",
32
+ "migrate",
33
+ "migration",
34
+ "mirror",
35
+ "replicate",
36
+ "replication",
37
+ "media-sync",
38
+ "synchronization",
39
+ "cross-environment",
40
+ "data-transfer",
41
+ "headless-cms",
42
+ "content-management"
43
+ ],
44
+ "exports": {
45
+ "./strapi-admin": {
46
+ "source": "./admin/src/index.js",
47
+ "import": "./admin/src/index.js",
48
+ "require": "./admin/src/index.js",
49
+ "default": "./admin/src/index.js"
50
+ },
51
+ "./strapi-server": {
52
+ "source": "./server/src/index.js",
53
+ "import": "./server/src/index.js",
54
+ "require": "./server/src/index.js",
55
+ "default": "./server/src/index.js"
56
+ },
57
+ "./package.json": "./package.json"
58
+ },
59
+ "files": [
60
+ "admin/",
61
+ "server/",
62
+ "README.md",
63
+ "LICENSE"
64
+ ],
65
+ "scripts": {
66
+ "test": "echo \"No tests yet\" && exit 0"
67
+ },
68
+ "peerDependencies": {
69
+ "@strapi/strapi": "^5.0.0",
70
+ "react": "^17.0.0 || ^18.0.0",
71
+ "react-dom": "^17.0.0 || ^18.0.0",
72
+ "react-router-dom": "^6.0.0"
73
+ },
74
+ "engines": {
75
+ "node": ">=20.0.0",
76
+ "npm": ">=6.0.0"
77
+ },
78
+ "strapi": {
79
+ "name": "strapi-content-sync-pro",
80
+ "description": "Copy, migrate, and live-sync content, media, and data between Strapi environments",
81
+ "kind": "plugin",
82
+ "displayName": "Content Sync Pro"
83
+ }
84
+ }
@@ -0,0 +1,151 @@
1
+ 'use strict';
2
+
3
+ const { ensureSyncId } = require('./utils/sync-id');
4
+ const { isRemoteUpdate } = require('./utils/sync-guard');
5
+
6
+ const bootstrap = ({ strapi }) => {
7
+ /**
8
+ * Check if live sync is enabled for a content type
9
+ */
10
+ const isLiveSyncEnabled = async (contentTypeUid) => {
11
+ try {
12
+ const executionService = strapi.plugin('strapi-content-sync-pro').service('syncExecution');
13
+ const profilesService = strapi.plugin('strapi-content-sync-pro').service('syncProfiles');
14
+
15
+ // Get active profile for this content type
16
+ const profile = await profilesService.getActiveProfileForContentType(contentTypeUid);
17
+ if (!profile) return false;
18
+
19
+ // Get execution settings
20
+ const execSettings = await executionService.getProfileExecutionSettings(profile.id);
21
+
22
+ // Check if live mode is enabled
23
+ return execSettings.executionMode === 'live' && execSettings.enabled;
24
+ } catch (err) {
25
+ strapi.log.error(`[data-sync] Error checking live sync: ${err.message}`);
26
+ return false;
27
+ }
28
+ };
29
+
30
+ /**
31
+ * Execute live sync for a record
32
+ */
33
+ const executeLiveSync = async (contentTypeUid, record, action) => {
34
+ try {
35
+ const executionService = strapi.plugin('strapi-content-sync-pro').service('syncExecution');
36
+ const profilesService = strapi.plugin('strapi-content-sync-pro').service('syncProfiles');
37
+ const syncService = strapi.plugin('strapi-content-sync-pro').service('sync');
38
+ const logService = strapi.plugin('strapi-content-sync-pro').service('syncLog');
39
+
40
+ const profile = await profilesService.getActiveProfileForContentType(contentTypeUid);
41
+ if (!profile) return;
42
+
43
+ // Log the live sync trigger
44
+ await logService.log({
45
+ action: `live_${action}`,
46
+ contentType: contentTypeUid,
47
+ syncId: record.syncId,
48
+ direction: profile.direction,
49
+ status: 'info',
50
+ message: `Live sync triggered: ${action} on ${contentTypeUid}`,
51
+ details: { profileId: profile.id, recordId: record.id },
52
+ });
53
+
54
+ // Push the record based on profile direction
55
+ if (profile.direction === 'push' || profile.direction === 'both') {
56
+ await syncService.pushRecord(contentTypeUid, record);
57
+ }
58
+ } catch (err) {
59
+ strapi.log.error(`[data-sync] Live sync failed: ${err.message}`);
60
+ }
61
+ };
62
+
63
+ // Subscribe to DB lifecycle events for all content types
64
+ strapi.db.lifecycles.subscribe({
65
+ /**
66
+ * Automatically generate a syncId (UUID) for every new
67
+ * record in an api:: content type that does not already have one.
68
+ */
69
+ async beforeCreate(event) {
70
+ const { model, params } = event;
71
+ if (!model.uid.startsWith('api::')) return;
72
+
73
+ if (params.data) {
74
+ ensureSyncId(params.data);
75
+ }
76
+ },
77
+
78
+ /**
79
+ * After a local record is created, push it to the remote
80
+ * instance if live sync is enabled.
81
+ */
82
+ async afterCreate(event) {
83
+ const { model, result } = event;
84
+ if (!model.uid.startsWith('api::')) return;
85
+
86
+ const key = `${model.uid}:${result.syncId}`;
87
+ if (isRemoteUpdate(key)) return;
88
+
89
+ // Check if live sync is enabled
90
+ const liveEnabled = await isLiveSyncEnabled(model.uid);
91
+ if (!liveEnabled) return;
92
+
93
+ await executeLiveSync(model.uid, result, 'create');
94
+ },
95
+
96
+ /**
97
+ * After a local record is updated, push it to the remote
98
+ * instance if live sync is enabled.
99
+ */
100
+ async afterUpdate(event) {
101
+ const { model, result } = event;
102
+ if (!model.uid.startsWith('api::')) return;
103
+
104
+ const key = `${model.uid}:${result.syncId}`;
105
+ if (isRemoteUpdate(key)) return;
106
+
107
+ // Check if live sync is enabled
108
+ const liveEnabled = await isLiveSyncEnabled(model.uid);
109
+ if (!liveEnabled) return;
110
+
111
+ await executeLiveSync(model.uid, result, 'update');
112
+ },
113
+
114
+ /**
115
+ * After a local record is deleted, notify the remote
116
+ * instance if live sync is enabled.
117
+ */
118
+ async afterDelete(event) {
119
+ const { model, result } = event;
120
+ if (!model.uid.startsWith('api::')) return;
121
+ if (!result?.syncId) return;
122
+
123
+ const key = `${model.uid}:${result.syncId}`;
124
+ if (isRemoteUpdate(key)) return;
125
+
126
+ // Check if live sync is enabled
127
+ const liveEnabled = await isLiveSyncEnabled(model.uid);
128
+ if (!liveEnabled) return;
129
+
130
+ await executeLiveSync(model.uid, result, 'delete');
131
+ },
132
+ });
133
+
134
+ // Initialize scheduled syncs after Strapi is ready
135
+ strapi.server.use(async (ctx, next) => {
136
+ await next();
137
+ });
138
+
139
+ // Defer scheduler initialization
140
+ setImmediate(async () => {
141
+ try {
142
+ const executionService = strapi.plugin('strapi-content-sync-pro').service('syncExecution');
143
+ await executionService.initializeSchedulers();
144
+ strapi.log.info('[data-sync] Scheduled sync jobs initialized');
145
+ } catch (err) {
146
+ strapi.log.error(`[data-sync] Failed to initialize schedulers: ${err.message}`);
147
+ }
148
+ });
149
+ };
150
+
151
+ module.exports = bootstrap;
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ default: {},
5
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ const syncLogSchema = require('./sync-log/schema.json');
4
+
5
+ module.exports = {
6
+ 'sync-log': { schema: syncLogSchema },
7
+ };
@@ -0,0 +1,24 @@
1
+ {
2
+ "kind": "collectionType",
3
+ "collectionName": "sync_logs",
4
+ "info": {
5
+ "singularName": "sync-log",
6
+ "pluralName": "sync-logs",
7
+ "displayName": "Sync Log"
8
+ },
9
+ "options": {},
10
+ "pluginOptions": {
11
+ "content-manager": { "visible": false },
12
+ "content-type-builder": { "visible": false }
13
+ },
14
+ "attributes": {
15
+ "action": { "type": "string" },
16
+ "contentType": { "type": "string" },
17
+ "recordId": { "type": "string" },
18
+ "syncId": { "type": "string" },
19
+ "direction": { "type": "string" },
20
+ "status": { "type": "string" },
21
+ "message": { "type": "text" },
22
+ "details": { "type": "json" }
23
+ }
24
+ }
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ const PLUGIN_ID = 'strapi-content-sync-pro';
4
+
5
+ module.exports = ({ strapi }) => ({
6
+ /**
7
+ * GET /alerts/settings
8
+ * Get alert settings
9
+ */
10
+ async getSettings(ctx) {
11
+ try {
12
+ const settings = await strapi.plugin(PLUGIN_ID).service('alerts').getSettings();
13
+ ctx.body = { data: settings };
14
+ } catch (err) {
15
+ ctx.throw(500, err.message);
16
+ }
17
+ },
18
+
19
+ /**
20
+ * PUT /alerts/settings
21
+ * Update alert settings
22
+ */
23
+ async updateSettings(ctx) {
24
+ const body = ctx.request.body;
25
+ try {
26
+ const settings = await strapi.plugin(PLUGIN_ID).service('alerts').updateSettings(body);
27
+ ctx.body = { data: settings };
28
+ } catch (err) {
29
+ ctx.throw(400, err.message);
30
+ }
31
+ },
32
+
33
+ /**
34
+ * POST /alerts/test/:channel
35
+ * Test an alert channel
36
+ */
37
+ async testChannel(ctx) {
38
+ const { channel } = ctx.params;
39
+ try {
40
+ const result = await strapi.plugin(PLUGIN_ID).service('alerts').testChannel(channel);
41
+ ctx.body = { data: result };
42
+ } catch (err) {
43
+ ctx.throw(400, err.message);
44
+ }
45
+ },
46
+
47
+ /**
48
+ * GET /alerts/stats
49
+ * Get alert statistics
50
+ */
51
+ async getStats(ctx) {
52
+ try {
53
+ const stats = strapi.plugin(PLUGIN_ID).service('alerts').getAlertStats();
54
+ ctx.body = { data: stats };
55
+ } catch (err) {
56
+ ctx.throw(500, err.message);
57
+ }
58
+ },
59
+ });