strapi-content-sync-pro 1.0.6 → 1.0.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strapi-content-sync-pro",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
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
5
  "license": "MIT",
6
6
  "author": {
@@ -139,6 +139,12 @@ const bootstrap = ({ strapi }) => {
139
139
  // Defer scheduler initialization
140
140
  setImmediate(async () => {
141
141
  try {
142
+ const workflowNotificationsService = strapi.plugin('strapi-content-sync-pro').service('workflowNotifications');
143
+ const seedResult = await workflowNotificationsService.seedTemplates();
144
+ if (seedResult.seeded) {
145
+ strapi.log.info(`[data-sync] Seeded ${seedResult.total} workflow notification templates`);
146
+ }
147
+
142
148
  const executionService = strapi.plugin('strapi-content-sync-pro').service('syncExecution');
143
149
  await executionService.initializeSchedulers();
144
150
  strapi.log.info('[data-sync] Scheduled sync jobs initialized');
@@ -2,8 +2,10 @@
2
2
 
3
3
  const syncLogSchema = require('./sync-log/schema.json');
4
4
  const syncRunReportSchema = require('./sync-run-report/schema.json');
5
+ const workflowNotificationSchema = require('./workflow-notification/schema.json');
5
6
 
6
7
  module.exports = {
7
8
  'sync-log': { schema: syncLogSchema },
8
9
  'sync-run-report': { schema: syncRunReportSchema },
10
+ 'workflow-notification': { schema: workflowNotificationSchema },
9
11
  };
@@ -0,0 +1,26 @@
1
+ {
2
+ "kind": "collectionType",
3
+ "collectionName": "workflow_notifications",
4
+ "info": {
5
+ "singularName": "workflow-notification",
6
+ "pluralName": "workflow-notifications",
7
+ "displayName": "Workflow Notification"
8
+ },
9
+ "options": {},
10
+ "pluginOptions": {
11
+ "content-manager": { "visible": false },
12
+ "content-type-builder": { "visible": false }
13
+ },
14
+ "attributes": {
15
+ "sourceApp": { "type": "enumeration", "enum": ["web", "web-user-app"] },
16
+ "workflow": { "type": "enumeration", "enum": ["order", "purchase"] },
17
+ "event": { "type": "string" },
18
+ "title": { "type": "string", "required": true },
19
+ "message": { "type": "text", "required": true },
20
+ "status": { "type": "enumeration", "enum": ["pending", "sent", "failed"], "default": "pending" },
21
+ "recipient": { "type": "string" },
22
+ "orderId": { "type": "string" },
23
+ "purchaseId": { "type": "string" },
24
+ "metadata": { "type": "json" }
25
+ }
26
+ }
@@ -11,6 +11,7 @@ const syncExecution = require('./sync-execution');
11
11
  const syncEnforcement = require('./sync-enforcement');
12
12
  const syncMedia = require('./sync-media');
13
13
  const alerts = require('./alerts');
14
+ const workflowNotifications = require('./workflow-notifications');
14
15
  const dependencies = require('./dependencies');
15
16
  const syncStats = require('./sync-stats');
16
17
  const bulkTransfer = require('./bulk-transfer');
@@ -27,6 +28,7 @@ module.exports = {
27
28
  syncEnforcement,
28
29
  syncMedia,
29
30
  alerts,
31
+ workflowNotifications,
30
32
  dependencies,
31
33
  syncStats,
32
34
  bulkTransfer,
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ const PLUGIN_ID = 'strapi-content-sync-pro';
4
+
5
+ module.exports = ({ strapi }) => ({
6
+ async emit(ctx) {
7
+ try {
8
+ const payload = ctx.request.body || {};
9
+ const data = await strapi.plugin(PLUGIN_ID).service('workflowNotifications').emit(payload);
10
+ ctx.body = { data };
11
+ } catch (err) {
12
+ ctx.throw(400, err.message);
13
+ }
14
+ },
15
+
16
+ async find(ctx) {
17
+ try {
18
+ const { page, pageSize, sourceApp, workflow, event, status } = ctx.query;
19
+ const result = await strapi.plugin(PLUGIN_ID).service('workflowNotifications').list({
20
+ page: page ? parseInt(page, 10) : 1,
21
+ pageSize: pageSize ? parseInt(pageSize, 10) : 25,
22
+ sourceApp,
23
+ workflow,
24
+ event,
25
+ status,
26
+ });
27
+
28
+ ctx.body = result;
29
+ } catch (err) {
30
+ ctx.throw(500, err.message);
31
+ }
32
+ },
33
+
34
+ async getTemplates(ctx) {
35
+ try {
36
+ const templates = await strapi.plugin(PLUGIN_ID).service('workflowNotifications').getTemplates();
37
+ ctx.body = { data: templates };
38
+ } catch (err) {
39
+ ctx.throw(500, err.message);
40
+ }
41
+ },
42
+
43
+ async seedTemplates(ctx) {
44
+ try {
45
+ const result = await strapi.plugin(PLUGIN_ID).service('workflowNotifications').seedTemplates();
46
+ ctx.body = { data: result };
47
+ } catch (err) {
48
+ ctx.throw(500, err.message);
49
+ }
50
+ },
51
+ });
@@ -8,6 +8,7 @@ const contentTypes = require('./content-types');
8
8
  const controllers = require('./controllers');
9
9
  const routes = require('./routes');
10
10
  const services = require('./services');
11
+ const middlewares = require('./middlewares');
11
12
 
12
13
  module.exports = {
13
14
  register,
@@ -18,4 +19,5 @@ module.exports = {
18
19
  controllers,
19
20
  services,
20
21
  contentTypes,
22
+ middlewares,
21
23
  };
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ const verifySignature = require('./verify-signature');
4
+
5
+ /**
6
+ * Strapi v5 plugin middleware registry.
7
+ * Each entry must be a middleware factory: (config, { strapi }) => (ctx, next) => {...}.
8
+ * Middlewares are referenced by name string in route configs:
9
+ * middlewares: ['plugin::strapi-content-sync-pro.verifySignature']
10
+ */
11
+ module.exports = {
12
+ verifySignature,
13
+ };
@@ -1,32 +1,32 @@
1
- 'use strict';
2
-
3
- const { verifySignature } = require('../utils/hmac');
4
-
5
- /**
6
- * Koa middleware that validates the HMAC signature sent by a remote
7
- * Strapi instance. Used on the /receive endpoint.
8
- */
9
- module.exports = async (ctx, next) => {
10
- const signature = ctx.request.headers['x-sync-signature'];
11
- const timestamp = ctx.request.headers['x-sync-timestamp'];
12
-
13
- if (!signature || !timestamp) {
14
- return ctx.unauthorized('Missing x-sync-signature or x-sync-timestamp header');
15
- }
16
-
17
- const configService = strapi.plugin('strapi-content-sync-pro').service('config');
18
- const serverConfig = await configService.getConfig({ safe: false });
19
-
20
- if (!serverConfig || !serverConfig.sharedSecret) {
21
- return ctx.unauthorized('Server not configured for sync');
22
- }
23
-
24
- const body = ctx.request.body || {};
25
- const isValid = verifySignature(body, serverConfig.sharedSecret, signature, timestamp);
26
-
27
- if (!isValid) {
28
- return ctx.unauthorized('Invalid sync signature');
29
- }
30
-
31
- await next();
32
- };
1
+ 'use strict';
2
+
3
+ const { verifySignature } = require('../utils/hmac');
4
+
5
+ /**
6
+ * Strapi v5 middleware factory. Validates the HMAC signature sent by a
7
+ * remote Strapi instance. Used on the /receive endpoint.
8
+ */
9
+ module.exports = (config, { strapi }) => async (ctx, next) => {
10
+ const signature = ctx.request.headers['x-sync-signature'];
11
+ const timestamp = ctx.request.headers['x-sync-timestamp'];
12
+
13
+ if (!signature || !timestamp) {
14
+ return ctx.unauthorized('Missing x-sync-signature or x-sync-timestamp header');
15
+ }
16
+
17
+ const configService = strapi.plugin('strapi-content-sync-pro').service('config');
18
+ const serverConfig = await configService.getConfig({ safe: false });
19
+
20
+ if (!serverConfig || !serverConfig.sharedSecret) {
21
+ return ctx.unauthorized('Server not configured for sync');
22
+ }
23
+
24
+ const body = ctx.request.body || {};
25
+ const isValid = verifySignature(body, serverConfig.sharedSecret, signature, timestamp);
26
+
27
+ if (!isValid) {
28
+ return ctx.unauthorized('Invalid sync signature');
29
+ }
30
+
31
+ await next();
32
+ };
@@ -1,7 +1,5 @@
1
1
  'use strict';
2
2
 
3
- const verifySignature = require('../middlewares/verify-signature');
4
-
5
3
  /**
6
4
  * Strapi v5 plugin routes MUST be split between:
7
5
  * - content-api: exposed under /api/<plugin>/... (remote server calls)
@@ -12,7 +10,11 @@ const contentApiRoutes = [
12
10
  { method: 'GET', path: '/ping', handler: 'ping.index', config: { policies: [], auth: false } },
13
11
  { method: 'GET', path: '/enforcement/local-info', handler: 'syncEnforcement.getLocalInfo', config: { policies: [] } },
14
12
  { method: 'GET', path: '/enforcement/schema/:uid', handler: 'syncEnforcement.getLocalSchema', config: { policies: [] } },
15
- { method: 'POST', path: '/receive', handler: 'sync.receive', config: { policies: [], auth: false, middlewares: [verifySignature] } },
13
+ { method: 'POST', path: '/receive', handler: 'sync.receive', config: { policies: [], auth: false, middlewares: ['plugin::strapi-content-sync-pro.verifySignature'] } },
14
+ { method: 'POST', path: '/workflow-notifications/emit', handler: 'workflowNotifications.emit', config: { policies: [], auth: false } },
15
+ { method: 'GET', path: '/workflow-notifications', handler: 'workflowNotifications.find', config: { policies: [] } },
16
+ { method: 'GET', path: '/workflow-notifications/templates', handler: 'workflowNotifications.getTemplates', config: { policies: [] } },
17
+ { method: 'POST', path: '/workflow-notifications/seed', handler: 'workflowNotifications.seedTemplates', config: { policies: [] } },
16
18
 
17
19
  // Media morph-link sync (called by the peer instance during runProfile)
18
20
  { method: 'GET', path: '/media-sync/morph-links', handler: 'syncMedia.getMorphLinks', config: { policies: [] } },
@@ -83,6 +85,11 @@ const adminRoutes = [
83
85
  { method: 'POST', path: '/alerts/test/:channel', handler: 'alerts.testChannel', config: { policies: [] } },
84
86
  { method: 'GET', path: '/alerts/stats', handler: 'alerts.getStats', config: { policies: [] } },
85
87
 
88
+ // Workflow notifications
89
+ { method: 'GET', path: '/workflow-notifications', handler: 'workflowNotifications.find', config: { policies: [] } },
90
+ { method: 'GET', path: '/workflow-notifications/templates', handler: 'workflowNotifications.getTemplates', config: { policies: [] } },
91
+ { method: 'POST', path: '/workflow-notifications/seed', handler: 'workflowNotifications.seedTemplates', config: { policies: [] } },
92
+
86
93
  // Media sync
87
94
  { method: 'GET', path: '/media-sync/profiles', handler: 'syncMedia.getProfiles', config: { policies: [] } },
88
95
  { method: 'GET', path: '/media-sync/profiles/:id', handler: 'syncMedia.getProfile', config: { policies: [] } },
@@ -12,6 +12,7 @@ const dependencyResolver = require('./dependency-resolver');
12
12
  const syncEnforcement = require('./sync-enforcement');
13
13
  const syncMedia = require('./sync-media');
14
14
  const alerts = require('./alerts');
15
+ const workflowNotifications = require('./workflow-notifications');
15
16
  const syncStats = require('./sync-stats');
16
17
  const bulkTransfer = require('./bulk-transfer');
17
18
 
@@ -28,6 +29,7 @@ module.exports = {
28
29
  syncEnforcement,
29
30
  syncMedia,
30
31
  alerts,
32
+ workflowNotifications,
31
33
  syncStats,
32
34
  bulkTransfer,
33
35
  };
@@ -0,0 +1,145 @@
1
+ 'use strict';
2
+
3
+ const CONTENT_TYPE_UID = 'plugin::strapi-content-sync-pro.workflow-notification';
4
+ const STORE_KEY = 'workflow-notification-templates';
5
+
6
+ const DEFAULT_TEMPLATES = [
7
+ {
8
+ sourceApp: 'web',
9
+ workflow: 'order',
10
+ event: 'order_created',
11
+ title: 'New order placed',
12
+ message: 'Order {{orderId}} placed by {{customerName}} for {{amount}}.',
13
+ },
14
+ {
15
+ sourceApp: 'web',
16
+ workflow: 'order',
17
+ event: 'order_paid',
18
+ title: 'Order payment received',
19
+ message: 'Payment received for order {{orderId}}.',
20
+ },
21
+ {
22
+ sourceApp: 'web-user-app',
23
+ workflow: 'purchase',
24
+ event: 'purchase_initiated',
25
+ title: 'Purchase initiated',
26
+ message: 'User {{userId}} initiated purchase {{purchaseId}}.',
27
+ },
28
+ {
29
+ sourceApp: 'web-user-app',
30
+ workflow: 'purchase',
31
+ event: 'purchase_completed',
32
+ title: 'Purchase completed',
33
+ message: 'Purchase {{purchaseId}} completed successfully for {{userId}}.',
34
+ },
35
+ ];
36
+
37
+ module.exports = ({ strapi }) => ({
38
+ getStore() {
39
+ return strapi.store({ type: 'plugin', name: 'strapi-content-sync-pro' });
40
+ },
41
+
42
+ interpolate(template, payload = {}) {
43
+ return template.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_, key) => {
44
+ const value = payload[key];
45
+ return value === undefined || value === null ? '' : String(value);
46
+ });
47
+ },
48
+
49
+ async seedTemplates() {
50
+ const store = this.getStore();
51
+ const existing = await store.get({ key: STORE_KEY });
52
+
53
+ if (Array.isArray(existing) && existing.length > 0) {
54
+ return { seeded: false, total: existing.length };
55
+ }
56
+
57
+ await store.set({ key: STORE_KEY, value: DEFAULT_TEMPLATES });
58
+ return { seeded: true, total: DEFAULT_TEMPLATES.length };
59
+ },
60
+
61
+ async getTemplates() {
62
+ const store = this.getStore();
63
+ const templates = await store.get({ key: STORE_KEY });
64
+ if (Array.isArray(templates) && templates.length > 0) {
65
+ return templates;
66
+ }
67
+
68
+ await this.seedTemplates();
69
+ return DEFAULT_TEMPLATES;
70
+ },
71
+
72
+ async findTemplate({ sourceApp, workflow, event }) {
73
+ const templates = await this.getTemplates();
74
+ return templates.find((template) => (
75
+ template.sourceApp === sourceApp
76
+ && template.workflow === workflow
77
+ && template.event === event
78
+ ));
79
+ },
80
+
81
+ async emit(payload = {}) {
82
+ const { sourceApp, workflow, event, recipient, metadata } = payload;
83
+
84
+ if (!sourceApp || !workflow || !event) {
85
+ throw new Error('sourceApp, workflow, and event are required');
86
+ }
87
+
88
+ const template = await this.findTemplate({ sourceApp, workflow, event });
89
+ if (!template) {
90
+ throw new Error(`No notification template found for ${sourceApp}/${workflow}/${event}`);
91
+ }
92
+
93
+ const title = this.interpolate(template.title, payload);
94
+ const message = this.interpolate(template.message, payload);
95
+
96
+ const entry = await strapi.documents(CONTENT_TYPE_UID).create({
97
+ data: {
98
+ sourceApp,
99
+ workflow,
100
+ event,
101
+ title,
102
+ message,
103
+ recipient: recipient || '',
104
+ orderId: payload.orderId || '',
105
+ purchaseId: payload.purchaseId || '',
106
+ status: 'pending',
107
+ metadata: metadata || payload,
108
+ },
109
+ });
110
+
111
+ return entry;
112
+ },
113
+
114
+ async list({ page = 1, pageSize = 25, sourceApp, workflow, event, status } = {}) {
115
+ const filters = {};
116
+ if (sourceApp) filters.sourceApp = sourceApp;
117
+ if (workflow) filters.workflow = workflow;
118
+ if (event) filters.event = event;
119
+ if (status) filters.status = status;
120
+
121
+ const start = (page - 1) * pageSize;
122
+
123
+ const [entries, total] = await Promise.all([
124
+ strapi.documents(CONTENT_TYPE_UID).findMany({
125
+ filters,
126
+ sort: { createdAt: 'desc' },
127
+ limit: pageSize,
128
+ start,
129
+ }),
130
+ strapi.documents(CONTENT_TYPE_UID).count({ filters }),
131
+ ]);
132
+
133
+ return {
134
+ data: entries,
135
+ meta: {
136
+ pagination: {
137
+ page,
138
+ pageSize,
139
+ pageCount: Math.ceil(total / pageSize),
140
+ total,
141
+ },
142
+ },
143
+ };
144
+ },
145
+ });