backend-manager 5.0.162 → 5.0.163
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/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
14
14
|
- `Fixed` for any bug fixes.
|
|
15
15
|
- `Security` in case of vulnerabilities.
|
|
16
16
|
|
|
17
|
+
# [5.0.163] - 2026-03-18
|
|
18
|
+
### Changed
|
|
19
|
+
- Refactored campaign POST/PUT routes to generic field passthrough — schema-validated fields flow through automatically via shared `buildCampaignDoc()` utility, no manual field assignments needed
|
|
20
|
+
- Extracted `normalizeSendAt()` and `DOC_LEVEL_FIELDS` into `routes/marketing/campaign/utils.js`
|
|
21
|
+
|
|
17
22
|
# [5.0.161] - 2026-03-18
|
|
18
23
|
### Added
|
|
19
24
|
- Port conflict detection in `serve` command — checks and kills blocking processes before starting Firebase server
|
package/package.json
CHANGED
|
@@ -9,9 +9,8 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Content is markdown — converted to HTML at send time per provider.
|
|
11
11
|
*/
|
|
12
|
-
const _ = require('lodash');
|
|
13
|
-
const moment = require('moment');
|
|
14
12
|
const pushid = require('pushid');
|
|
13
|
+
const { buildCampaignDoc } = require('./utils');
|
|
15
14
|
|
|
16
15
|
module.exports = async ({ assistant, user, Manager, settings, analytics }) => {
|
|
17
16
|
|
|
@@ -24,55 +23,14 @@ module.exports = async ({ assistant, user, Manager, settings, analytics }) => {
|
|
|
24
23
|
|
|
25
24
|
const { admin } = Manager.libraries;
|
|
26
25
|
const campaignId = settings.id || pushid();
|
|
27
|
-
const now =
|
|
26
|
+
const { docFields, campaignSettings, now } = buildCampaignDoc(settings);
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
const sendAt = normalizeSendAt(settings.sendAt, now);
|
|
31
|
-
|
|
32
|
-
// Build the campaign document (settings nested, like emails-queue)
|
|
33
|
-
const campaignSettings = {};
|
|
34
|
-
|
|
35
|
-
// Required
|
|
36
|
-
campaignSettings.name = settings.name;
|
|
37
|
-
campaignSettings.subject = settings.subject;
|
|
38
|
-
|
|
39
|
-
// Content
|
|
40
|
-
if (settings.preheader) { campaignSettings.preheader = settings.preheader; }
|
|
41
|
-
if (settings.template && settings.template !== 'default') { campaignSettings.template = settings.template; }
|
|
42
|
-
if (settings.content) { campaignSettings.content = settings.content; }
|
|
43
|
-
if (settings.data && Object.keys(settings.data).length) { campaignSettings.data = settings.data; }
|
|
44
|
-
|
|
45
|
-
// Targeting
|
|
46
|
-
if (settings.lists && settings.lists.length) { campaignSettings.lists = settings.lists; }
|
|
47
|
-
if (settings.segments && settings.segments.length) { campaignSettings.segments = settings.segments; }
|
|
48
|
-
if (settings.excludeSegments && settings.excludeSegments.length) { campaignSettings.excludeSegments = settings.excludeSegments; }
|
|
49
|
-
if (settings.all) { campaignSettings.all = true; }
|
|
50
|
-
|
|
51
|
-
// UTM
|
|
52
|
-
if (settings.utm && Object.keys(settings.utm).length) { campaignSettings.utm = settings.utm; }
|
|
53
|
-
|
|
54
|
-
// Config
|
|
55
|
-
if (settings.sender) { campaignSettings.sender = settings.sender; }
|
|
56
|
-
if (settings.providers && settings.providers.length) { campaignSettings.providers = settings.providers; }
|
|
57
|
-
if (settings.group) { campaignSettings.group = settings.group; }
|
|
58
|
-
if (settings.categories && settings.categories.length) { campaignSettings.categories = settings.categories; }
|
|
59
|
-
|
|
60
|
-
// Clone and clean undefined values for Firestore
|
|
61
|
-
const settingsCloned = _.cloneDeepWith(campaignSettings, (value) => {
|
|
62
|
-
if (typeof value === 'undefined') {
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
const isFuture = sendAt > now.unix();
|
|
68
|
-
const type = settings.type || 'email';
|
|
28
|
+
const isFuture = docFields.sendAt > now.unix();
|
|
69
29
|
|
|
70
30
|
const doc = {
|
|
71
|
-
|
|
72
|
-
|
|
31
|
+
...docFields,
|
|
32
|
+
settings: campaignSettings,
|
|
73
33
|
status: 'pending',
|
|
74
|
-
type,
|
|
75
|
-
...(settings.recurrence ? { recurrence: settings.recurrence } : {}),
|
|
76
34
|
metadata: {
|
|
77
35
|
created: {
|
|
78
36
|
timestamp: now.toISOString(),
|
|
@@ -88,12 +46,12 @@ module.exports = async ({ assistant, user, Manager, settings, analytics }) => {
|
|
|
88
46
|
// Save to Firestore
|
|
89
47
|
await admin.firestore().doc(`marketing-campaigns/${campaignId}`).set(doc);
|
|
90
48
|
|
|
91
|
-
assistant.log('marketing/campaign created:', { campaignId, sendAt, isFuture, type });
|
|
49
|
+
assistant.log('marketing/campaign created:', { campaignId, sendAt: docFields.sendAt, isFuture, type: docFields.type });
|
|
92
50
|
|
|
93
51
|
// If sendAt is now/past, fire immediately
|
|
94
52
|
let results = null;
|
|
95
53
|
|
|
96
|
-
if (!isFuture && type === 'email') {
|
|
54
|
+
if (!isFuture && docFields.type === 'email') {
|
|
97
55
|
const mailer = Manager.Email(assistant);
|
|
98
56
|
results = await mailer.sendCampaign({ ...campaignSettings, sendAt: 'now' });
|
|
99
57
|
|
|
@@ -117,45 +75,14 @@ module.exports = async ({ assistant, user, Manager, settings, analytics }) => {
|
|
|
117
75
|
// Analytics
|
|
118
76
|
analytics.event('marketing/campaign', {
|
|
119
77
|
action: isFuture ? 'schedule' : 'send',
|
|
120
|
-
type,
|
|
78
|
+
type: docFields.type,
|
|
121
79
|
});
|
|
122
80
|
|
|
123
81
|
return assistant.respond({
|
|
124
82
|
success: true,
|
|
125
83
|
id: campaignId,
|
|
126
84
|
status: isFuture ? 'pending' : (results ? 'sent' : 'pending'),
|
|
127
|
-
sendAt,
|
|
85
|
+
sendAt: docFields.sendAt,
|
|
128
86
|
providers: results,
|
|
129
87
|
});
|
|
130
88
|
};
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Normalize sendAt to unix timestamp.
|
|
134
|
-
* Accepts: 'now', ISO string, unix timestamp (number or string), undefined/empty.
|
|
135
|
-
* Defaults to now.
|
|
136
|
-
*/
|
|
137
|
-
function normalizeSendAt(sendAt, now) {
|
|
138
|
-
if (!sendAt || sendAt === 'now') {
|
|
139
|
-
return now.unix();
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Unix timestamp (number)
|
|
143
|
-
if (typeof sendAt === 'number') {
|
|
144
|
-
return sendAt;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Unix timestamp as string (all digits)
|
|
148
|
-
if (/^\d+$/.test(sendAt)) {
|
|
149
|
-
return parseInt(sendAt, 10);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// ISO string or other parseable date
|
|
153
|
-
const parsed = moment(sendAt);
|
|
154
|
-
|
|
155
|
-
if (parsed.isValid()) {
|
|
156
|
-
return parsed.unix();
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Fallback to now
|
|
160
|
-
return now.unix();
|
|
161
|
-
}
|
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
* Accepts any field from the POST schema. Only provided fields are updated.
|
|
6
6
|
* Changing sendAt reschedules the campaign (if still pending).
|
|
7
7
|
*/
|
|
8
|
-
const
|
|
9
|
-
const moment = require('moment');
|
|
8
|
+
const { buildCampaignDoc } = require('./utils');
|
|
10
9
|
|
|
11
10
|
module.exports = async ({ assistant, user, Manager, settings, analytics }) => {
|
|
12
11
|
|
|
@@ -39,8 +38,11 @@ module.exports = async ({ assistant, user, Manager, settings, analytics }) => {
|
|
|
39
38
|
return assistant.respond(`Cannot edit campaign with status "${existing.status}"`, { code: 400 });
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
// Build update
|
|
41
|
+
// Build update from provided fields using shared utility
|
|
42
|
+
const { docFields, campaignSettings } = buildCampaignDoc(settings);
|
|
43
|
+
|
|
43
44
|
const update = {
|
|
45
|
+
...docFields,
|
|
44
46
|
metadata: {
|
|
45
47
|
updated: {
|
|
46
48
|
timestamp: new Date().toISOString(),
|
|
@@ -49,44 +51,9 @@ module.exports = async ({ assistant, user, Manager, settings, analytics }) => {
|
|
|
49
51
|
},
|
|
50
52
|
};
|
|
51
53
|
|
|
52
|
-
//
|
|
53
|
-
if (
|
|
54
|
-
update.
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Update type if provided
|
|
58
|
-
if (settings.type) {
|
|
59
|
-
update.type = settings.type;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Update recurrence if provided
|
|
63
|
-
if (settings.recurrence !== undefined) {
|
|
64
|
-
update.recurrence = settings.recurrence;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Update settings fields — only merge what's provided
|
|
68
|
-
const settingsUpdate = {};
|
|
69
|
-
const settingsFields = [
|
|
70
|
-
'name', 'subject', 'preheader', 'template', 'content', 'data',
|
|
71
|
-
'lists', 'segments', 'excludeSegments', 'all',
|
|
72
|
-
'utm', 'sender', 'providers', 'group', 'categories',
|
|
73
|
-
];
|
|
74
|
-
|
|
75
|
-
for (const field of settingsFields) {
|
|
76
|
-
if (settings[field] !== undefined && settings[field] !== '') {
|
|
77
|
-
settingsUpdate[field] = settings[field];
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (Object.keys(settingsUpdate).length) {
|
|
82
|
-
// Clean undefined values for Firestore
|
|
83
|
-
const cleaned = _.cloneDeepWith(settingsUpdate, (value) => {
|
|
84
|
-
if (typeof value === 'undefined') {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
update.settings = { ...existing.settings, ...cleaned };
|
|
54
|
+
// Merge provided settings into existing
|
|
55
|
+
if (Object.keys(campaignSettings).length) {
|
|
56
|
+
update.settings = { ...existing.settings, ...campaignSettings };
|
|
90
57
|
}
|
|
91
58
|
|
|
92
59
|
await docRef.set(update, { merge: true });
|
|
@@ -103,20 +70,3 @@ module.exports = async ({ assistant, user, Manager, settings, analytics }) => {
|
|
|
103
70
|
campaign: { id: campaignId, ...updated.data() },
|
|
104
71
|
});
|
|
105
72
|
};
|
|
106
|
-
|
|
107
|
-
function normalizeSendAt(sendAt) {
|
|
108
|
-
if (!sendAt || sendAt === 'now') {
|
|
109
|
-
return Math.round(Date.now() / 1000);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (typeof sendAt === 'number') {
|
|
113
|
-
return sendAt;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (/^\d+$/.test(sendAt)) {
|
|
117
|
-
return parseInt(sendAt, 10);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const parsed = moment(sendAt);
|
|
121
|
-
return parsed.isValid() ? parsed.unix() : Math.round(Date.now() / 1000);
|
|
122
|
-
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for marketing campaign routes.
|
|
3
|
+
*/
|
|
4
|
+
const _ = require('lodash');
|
|
5
|
+
const moment = require('moment');
|
|
6
|
+
|
|
7
|
+
// Fields that live at the doc level, not inside doc.settings
|
|
8
|
+
const DOC_LEVEL_FIELDS = ['id', 'sendAt', 'type', 'recurrence', 'generator'];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Separate settings into doc-level fields and nested settings.
|
|
12
|
+
* Returns { docFields, campaignSettings }.
|
|
13
|
+
*/
|
|
14
|
+
function buildCampaignDoc(settings) {
|
|
15
|
+
const now = moment();
|
|
16
|
+
|
|
17
|
+
// Extract doc-level fields
|
|
18
|
+
const sendAt = normalizeSendAt(settings.sendAt, now);
|
|
19
|
+
const type = settings.type || 'email';
|
|
20
|
+
|
|
21
|
+
const docFields = {
|
|
22
|
+
sendAt,
|
|
23
|
+
type,
|
|
24
|
+
...(settings.recurrence ? { recurrence: settings.recurrence } : {}),
|
|
25
|
+
...(settings.generator ? { generator: settings.generator } : {}),
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Everything else goes into doc.settings — strip empties
|
|
29
|
+
const campaignSettings = _.omitBy(
|
|
30
|
+
_.omit(settings, DOC_LEVEL_FIELDS),
|
|
31
|
+
(v) => v === undefined || v === '' || (Array.isArray(v) && !v.length) || (_.isPlainObject(v) && !Object.keys(v).length),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return { docFields, campaignSettings, now };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Normalize sendAt to unix timestamp.
|
|
39
|
+
* Accepts: 'now', ISO string, unix timestamp (number or string), undefined/empty.
|
|
40
|
+
*/
|
|
41
|
+
function normalizeSendAt(sendAt, now) {
|
|
42
|
+
if (!sendAt || sendAt === 'now') {
|
|
43
|
+
return (now || moment()).unix();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (typeof sendAt === 'number') {
|
|
47
|
+
return sendAt;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (/^\d+$/.test(sendAt)) {
|
|
51
|
+
return parseInt(sendAt, 10);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const parsed = moment(sendAt);
|
|
55
|
+
return parsed.isValid() ? parsed.unix() : (now || moment()).unix();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { DOC_LEVEL_FIELDS, buildCampaignDoc, normalizeSendAt };
|