strapi-content-sync-pro 1.0.3 → 1.0.5
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 +33 -14
- package/admin/src/components/BulkTransferTab.jsx +880 -0
- package/admin/src/components/ConfigTab.jsx +81 -3
- package/admin/src/components/HelpTab.jsx +148 -5
- package/admin/src/components/MediaTab.jsx +141 -30
- package/admin/src/components/SyncTab.jsx +2 -0
- package/admin/src/pages/App/index.jsx +12 -1
- 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/package.json +13 -4
- package/server/src/controllers/bulk-transfer.js +141 -0
- package/server/src/controllers/config.js +76 -3
- package/server/src/controllers/index.js +2 -0
- package/server/src/controllers/sync-media.js +24 -0
- package/server/src/routes/index.js +18 -0
- package/server/src/services/bulk-transfer.js +837 -0
- package/server/src/services/index.js +2 -0
- package/server/src/services/sync-media.js +168 -32
- package/server/src/services/sync.js +137 -1
- 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
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strapi-content-sync-pro",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
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": {
|
|
7
|
-
"name": "Ejaz
|
|
7
|
+
"name": "Ejaz Hussain Arain",
|
|
8
8
|
"email": "eharain@yahoo.com",
|
|
9
|
-
"url": "https://
|
|
9
|
+
"url": "https://www.linkedin.com/in/ejazarain"
|
|
10
10
|
},
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
"strapi",
|
|
25
25
|
"strapi-plugin",
|
|
26
26
|
"strapi-v5",
|
|
27
|
+
"strapi pulgin to transfer content",
|
|
27
28
|
"sync",
|
|
29
|
+
"hot sync",
|
|
30
|
+
"real-time sync",
|
|
28
31
|
"live-sync",
|
|
29
32
|
"data-sync",
|
|
30
33
|
"content-sync",
|
|
@@ -39,7 +42,13 @@
|
|
|
39
42
|
"cross-environment",
|
|
40
43
|
"data-transfer",
|
|
41
44
|
"headless-cms",
|
|
42
|
-
"content-management"
|
|
45
|
+
"content-management",
|
|
46
|
+
"data transfer",
|
|
47
|
+
"bulk transfer",
|
|
48
|
+
"field-level policies",
|
|
49
|
+
"scheduling",
|
|
50
|
+
"alerts",
|
|
51
|
+
"bi-directional sync"
|
|
43
52
|
],
|
|
44
53
|
"exports": {
|
|
45
54
|
"./strapi-admin": {
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const PLUGIN_ID = 'strapi-content-sync-pro';
|
|
4
|
+
|
|
5
|
+
function svc(strapi) {
|
|
6
|
+
return strapi.plugin(PLUGIN_ID).service('bulkTransfer');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
module.exports = ({ strapi }) => ({
|
|
10
|
+
async preview(ctx) {
|
|
11
|
+
try {
|
|
12
|
+
const body = ctx.request.body || {};
|
|
13
|
+
const data = await svc(strapi).preview({
|
|
14
|
+
direction: body.direction,
|
|
15
|
+
scopes: body.scopes,
|
|
16
|
+
});
|
|
17
|
+
ctx.body = { data };
|
|
18
|
+
} catch (err) {
|
|
19
|
+
ctx.throw(400, err.message);
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
async start(ctx) {
|
|
24
|
+
try {
|
|
25
|
+
const body = ctx.request.body || {};
|
|
26
|
+
const data = await svc(strapi).start(body);
|
|
27
|
+
ctx.body = { data };
|
|
28
|
+
} catch (err) {
|
|
29
|
+
ctx.throw(400, err.message);
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
async next(ctx) {
|
|
34
|
+
try {
|
|
35
|
+
const { jobId } = ctx.params;
|
|
36
|
+
const data = await svc(strapi).next(jobId);
|
|
37
|
+
ctx.body = { data };
|
|
38
|
+
} catch (err) {
|
|
39
|
+
ctx.throw(400, err.message);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
async runAll(ctx) {
|
|
44
|
+
try {
|
|
45
|
+
const { jobId } = ctx.params;
|
|
46
|
+
const data = await svc(strapi).runToCompletion(jobId);
|
|
47
|
+
ctx.body = { data };
|
|
48
|
+
} catch (err) {
|
|
49
|
+
ctx.throw(400, err.message);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
async status(ctx) {
|
|
54
|
+
try {
|
|
55
|
+
const { jobId } = ctx.params;
|
|
56
|
+
const data = svc(strapi).getStatus(jobId);
|
|
57
|
+
if (!data) return ctx.notFound('Job not found');
|
|
58
|
+
ctx.body = { data };
|
|
59
|
+
} catch (err) {
|
|
60
|
+
ctx.throw(500, err.message);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
async cancel(ctx) {
|
|
65
|
+
try {
|
|
66
|
+
const { jobId } = ctx.params;
|
|
67
|
+
const data = await svc(strapi).cancel(jobId);
|
|
68
|
+
ctx.body = { data };
|
|
69
|
+
} catch (err) {
|
|
70
|
+
ctx.throw(400, err.message);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async pause(ctx) {
|
|
75
|
+
try {
|
|
76
|
+
const { jobId } = ctx.params;
|
|
77
|
+
const data = await svc(strapi).pause(jobId);
|
|
78
|
+
ctx.body = { data };
|
|
79
|
+
} catch (err) {
|
|
80
|
+
ctx.throw(400, err.message);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
async resume(ctx) {
|
|
85
|
+
try {
|
|
86
|
+
const { jobId } = ctx.params;
|
|
87
|
+
const data = await svc(strapi).resume(jobId);
|
|
88
|
+
ctx.body = { data };
|
|
89
|
+
} catch (err) {
|
|
90
|
+
ctx.throw(400, err.message);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
async list(ctx) {
|
|
95
|
+
try {
|
|
96
|
+
ctx.body = { data: svc(strapi).listJobs() };
|
|
97
|
+
} catch (err) {
|
|
98
|
+
ctx.throw(500, err.message);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
async history(ctx) {
|
|
103
|
+
try {
|
|
104
|
+
const data = await svc(strapi).getHistory();
|
|
105
|
+
ctx.body = { data };
|
|
106
|
+
} catch (err) {
|
|
107
|
+
ctx.throw(500, err.message);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
async clearHistory(ctx) {
|
|
112
|
+
try {
|
|
113
|
+
const data = await svc(strapi).clearHistory();
|
|
114
|
+
ctx.body = { data };
|
|
115
|
+
} catch (err) {
|
|
116
|
+
ctx.throw(500, err.message);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
async restart(ctx) {
|
|
121
|
+
try {
|
|
122
|
+
const { historyId } = ctx.params;
|
|
123
|
+
const body = ctx.request.body || {};
|
|
124
|
+
const data = await svc(strapi).restart(historyId, body);
|
|
125
|
+
ctx.body = { data };
|
|
126
|
+
} catch (err) {
|
|
127
|
+
ctx.throw(400, err.message);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
async resumeFromHistory(ctx) {
|
|
132
|
+
try {
|
|
133
|
+
const { historyId } = ctx.params;
|
|
134
|
+
const body = ctx.request.body || {};
|
|
135
|
+
const data = await svc(strapi).resumeFromHistory(historyId, body);
|
|
136
|
+
ctx.body = { data };
|
|
137
|
+
} catch (err) {
|
|
138
|
+
ctx.throw(400, err.message);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
});
|
|
@@ -198,12 +198,69 @@ module.exports = {
|
|
|
198
198
|
* Proxy login to remote Strapi and retrieve/create API token
|
|
199
199
|
*/
|
|
200
200
|
async remoteLogin(ctx) {
|
|
201
|
-
const { baseUrl, email, password } = ctx.request.body;
|
|
201
|
+
const { baseUrl: rawBaseUrl, email, password } = ctx.request.body;
|
|
202
202
|
|
|
203
|
-
if (!
|
|
203
|
+
if (!rawBaseUrl || !email || !password) {
|
|
204
204
|
return ctx.badRequest('baseUrl, email, and password are required');
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
// --- Normalize and validate the base URL so URL mistakes produce a clear message ---
|
|
208
|
+
let baseUrl = String(rawBaseUrl).trim();
|
|
209
|
+
if (!/^https?:\/\//i.test(baseUrl)) {
|
|
210
|
+
return ctx.badRequest(
|
|
211
|
+
`Invalid Server URL "${rawBaseUrl}": must start with http:// or https:// (e.g. http://localhost:4010)`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
// Strip trailing slashes and accidental /admin suffix
|
|
215
|
+
baseUrl = baseUrl.replace(/\/+$/, '').replace(/\/admin$/i, '');
|
|
216
|
+
let parsedUrl;
|
|
217
|
+
try {
|
|
218
|
+
parsedUrl = new URL(baseUrl);
|
|
219
|
+
} catch {
|
|
220
|
+
return ctx.badRequest(
|
|
221
|
+
`Invalid Server URL "${rawBaseUrl}": not a valid URL. Expected format: http(s)://host[:port]`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
if (!parsedUrl.hostname) {
|
|
225
|
+
return ctx.badRequest(`Invalid Server URL "${rawBaseUrl}": missing hostname`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// --- Pre-flight reachability check against /admin/init so wrong URL != "invalid credentials" ---
|
|
229
|
+
try {
|
|
230
|
+
const initResp = await fetch(`${baseUrl}/admin/init`, { method: 'GET' });
|
|
231
|
+
if (!initResp.ok) {
|
|
232
|
+
return ctx.throw(
|
|
233
|
+
502,
|
|
234
|
+
`Server URL "${baseUrl}" is reachable but did not respond as a Strapi admin (HTTP ${initResp.status} on /admin/init). Check that the URL points to the root of a Strapi v5 server (no /admin suffix) and that the admin panel is enabled.`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
const initData = await initResp.json().catch(() => null);
|
|
238
|
+
if (!initData || typeof initData !== 'object' || !('data' in initData)) {
|
|
239
|
+
return ctx.throw(
|
|
240
|
+
502,
|
|
241
|
+
`Server URL "${baseUrl}" did not return a valid Strapi /admin/init response. Please verify the URL points to a Strapi v5 instance.`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
if (initData.data && initData.data.hasAdmin === false) {
|
|
245
|
+
return ctx.throw(
|
|
246
|
+
400,
|
|
247
|
+
`Remote Strapi at "${baseUrl}" has no admin user yet. Create the first admin in the remote Strapi panel before generating a token.`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
} catch (err) {
|
|
251
|
+
if (err && err.status) throw err;
|
|
252
|
+
const code = err && (err.cause?.code || err.code);
|
|
253
|
+
const hint =
|
|
254
|
+
code === 'ECONNREFUSED'
|
|
255
|
+
? 'Connection refused — the server is not running or not listening on that port.'
|
|
256
|
+
: code === 'ENOTFOUND'
|
|
257
|
+
? 'Host not found — check the hostname/IP spelling and that it resolves from this machine.'
|
|
258
|
+
: code === 'ETIMEDOUT'
|
|
259
|
+
? 'Request timed out — check firewall, network, and that the port is reachable.'
|
|
260
|
+
: 'Network error while contacting the remote server.';
|
|
261
|
+
return ctx.throw(502, `Cannot reach "${baseUrl}": ${hint}${code ? ` (${code})` : ''}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
207
264
|
try {
|
|
208
265
|
// Step 1: Login to remote Strapi admin
|
|
209
266
|
const loginResponse = await fetch(`${baseUrl}/admin/login`, {
|
|
@@ -219,7 +276,23 @@ module.exports = {
|
|
|
219
276
|
|
|
220
277
|
if (!loginResponse.ok) {
|
|
221
278
|
const errorBody = await loginResponse.json().catch(() => ({}));
|
|
222
|
-
const
|
|
279
|
+
const remoteMsg = errorBody?.error?.message;
|
|
280
|
+
// Distinguish URL-vs-credential failures so users aren't misled
|
|
281
|
+
if (loginResponse.status === 404) {
|
|
282
|
+
return ctx.throw(
|
|
283
|
+
404,
|
|
284
|
+
`"${baseUrl}/admin/login" not found (HTTP 404). The Server URL likely points to the wrong path — use the Strapi root URL (e.g. http://localhost:4010), not an admin or API sub-path.`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
if (loginResponse.status === 405) {
|
|
288
|
+
return ctx.throw(
|
|
289
|
+
405,
|
|
290
|
+
`"${baseUrl}/admin/login" rejected the request method. The Server URL may be pointing to a proxy or non-Strapi endpoint.`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
const errorMessage = remoteMsg
|
|
294
|
+
? `Remote login failed: ${remoteMsg}. If you believe the credentials are correct, double-check the Server URL "${baseUrl}" is the right Strapi instance.`
|
|
295
|
+
: `Login failed with status ${loginResponse.status} at ${baseUrl}/admin/login`;
|
|
223
296
|
return ctx.throw(loginResponse.status, errorMessage);
|
|
224
297
|
}
|
|
225
298
|
|
|
@@ -13,6 +13,7 @@ const syncMedia = require('./sync-media');
|
|
|
13
13
|
const alerts = require('./alerts');
|
|
14
14
|
const dependencies = require('./dependencies');
|
|
15
15
|
const syncStats = require('./sync-stats');
|
|
16
|
+
const bulkTransfer = require('./bulk-transfer');
|
|
16
17
|
|
|
17
18
|
module.exports = {
|
|
18
19
|
ping,
|
|
@@ -28,4 +29,5 @@ module.exports = {
|
|
|
28
29
|
alerts,
|
|
29
30
|
dependencies,
|
|
30
31
|
syncStats,
|
|
32
|
+
bulkTransfer,
|
|
31
33
|
};
|
|
@@ -111,6 +111,30 @@ module.exports = ({ strapi }) => ({
|
|
|
111
111
|
}
|
|
112
112
|
},
|
|
113
113
|
|
|
114
|
+
async pauseProfile(ctx) {
|
|
115
|
+
try {
|
|
116
|
+
ctx.body = { data: await service(strapi).pauseProfile(ctx.params.id) };
|
|
117
|
+
} catch (err) {
|
|
118
|
+
ctx.throw(400, err.message);
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
async resumeProfile(ctx) {
|
|
123
|
+
try {
|
|
124
|
+
ctx.body = { data: await service(strapi).resumeProfile(ctx.params.id) };
|
|
125
|
+
} catch (err) {
|
|
126
|
+
ctx.throw(400, err.message);
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
async cancelProfile(ctx) {
|
|
131
|
+
try {
|
|
132
|
+
ctx.body = { data: await service(strapi).cancelProfile(ctx.params.id) };
|
|
133
|
+
} catch (err) {
|
|
134
|
+
ctx.throw(400, err.message);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
|
|
114
138
|
// ── Morph link sync (documentId-based mapping) ───────────────────────────
|
|
115
139
|
|
|
116
140
|
async getMorphLinks(ctx) {
|
|
@@ -91,6 +91,9 @@ const adminRoutes = [
|
|
|
91
91
|
{ method: 'DELETE', path: '/media-sync/profiles/:id', handler: 'syncMedia.deleteProfile', config: { policies: [] } },
|
|
92
92
|
{ method: 'POST', path: '/media-sync/profiles/:id/activate', handler: 'syncMedia.activateProfile', config: { policies: [] } },
|
|
93
93
|
{ method: 'POST', path: '/media-sync/profiles/:id/run', handler: 'syncMedia.runProfile', config: { policies: [] } },
|
|
94
|
+
{ method: 'POST', path: '/media-sync/profiles/:id/pause', handler: 'syncMedia.pauseProfile', config: { policies: [] } },
|
|
95
|
+
{ method: 'POST', path: '/media-sync/profiles/:id/resume', handler: 'syncMedia.resumeProfile', config: { policies: [] } },
|
|
96
|
+
{ method: 'POST', path: '/media-sync/profiles/:id/cancel', handler: 'syncMedia.cancelProfile', config: { policies: [] } },
|
|
94
97
|
{ method: 'POST', path: '/media-sync/run-active', handler: 'syncMedia.runActiveProfiles', config: { policies: [] } },
|
|
95
98
|
{ method: 'GET', path: '/media-sync/morph-links', handler: 'syncMedia.getMorphLinks', config: { policies: [] } },
|
|
96
99
|
{ method: 'POST', path: '/media-sync/morph-links/apply', handler: 'syncMedia.applyMorphLinks', config: { policies: [] } },
|
|
@@ -110,6 +113,21 @@ const adminRoutes = [
|
|
|
110
113
|
{ method: 'GET', path: '/dependencies/:uid/sync-order', handler: 'dependencies.getSyncOrder', config: { policies: [] } },
|
|
111
114
|
{ method: 'GET', path: '/dependencies/:uid/summary', handler: 'dependencies.getSummary', config: { policies: [] } },
|
|
112
115
|
{ method: 'POST', path: '/dependencies/clear-cache', handler: 'dependencies.clearCache', config: { policies: [] } },
|
|
116
|
+
|
|
117
|
+
// Bulk Transfer (one-click full pull / full push)
|
|
118
|
+
{ method: 'POST', path: '/bulk-transfer/preview', handler: 'bulkTransfer.preview', config: { policies: [] } },
|
|
119
|
+
{ method: 'POST', path: '/bulk-transfer/start', handler: 'bulkTransfer.start', config: { policies: [] } },
|
|
120
|
+
{ method: 'GET', path: '/bulk-transfer/jobs', handler: 'bulkTransfer.list', config: { policies: [] } },
|
|
121
|
+
{ method: 'GET', path: '/bulk-transfer/jobs/:jobId', handler: 'bulkTransfer.status', config: { policies: [] } },
|
|
122
|
+
{ method: 'POST', path: '/bulk-transfer/jobs/:jobId/next', handler: 'bulkTransfer.next', config: { policies: [] } },
|
|
123
|
+
{ method: 'POST', path: '/bulk-transfer/jobs/:jobId/run-all', handler: 'bulkTransfer.runAll', config: { policies: [] } },
|
|
124
|
+
{ method: 'POST', path: '/bulk-transfer/jobs/:jobId/pause', handler: 'bulkTransfer.pause', config: { policies: [] } },
|
|
125
|
+
{ method: 'POST', path: '/bulk-transfer/jobs/:jobId/resume', handler: 'bulkTransfer.resume', config: { policies: [] } },
|
|
126
|
+
{ method: 'POST', path: '/bulk-transfer/jobs/:jobId/cancel', handler: 'bulkTransfer.cancel', config: { policies: [] } },
|
|
127
|
+
{ method: 'GET', path: '/bulk-transfer/history', handler: 'bulkTransfer.history', config: { policies: [] } },
|
|
128
|
+
{ method: 'POST', path: '/bulk-transfer/history/clear', handler: 'bulkTransfer.clearHistory', config: { policies: [] } },
|
|
129
|
+
{ method: 'POST', path: '/bulk-transfer/history/:historyId/restart', handler: 'bulkTransfer.restart', config: { policies: [] } },
|
|
130
|
+
{ method: 'POST', path: '/bulk-transfer/history/:historyId/resume', handler: 'bulkTransfer.resumeFromHistory', config: { policies: [] } },
|
|
113
131
|
];
|
|
114
132
|
|
|
115
133
|
module.exports = {
|