payload-plugin-newsletter 0.16.3 → 0.16.6

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
@@ -1,3 +1,65 @@
1
+ ## [0.16.6] - 2025-07-29
2
+
3
+ ### Fixed
4
+ - **Critical: Update Sync Now Works** - Fixed the afterChange hook that was blocking update operations
5
+ - Removed the `operation !== 'create'` check that prevented the afterChange hook from running on updates
6
+ - Moved update sync logic from beforeChange to afterChange for proper architectural pattern
7
+ - Updates are now synced AFTER they're saved to Payload, ensuring consistency
8
+ - Provider sync failures no longer block Payload updates
9
+
10
+ ### Improved
11
+ - **Better Hook Architecture** - Sync operations now happen in the correct lifecycle stage
12
+ - beforeChange was architecturally wrong - if provider sync failed, data would be inconsistent
13
+ - afterChange ensures Payload data is saved first, then syncs to provider
14
+ - More resilient to network failures and API errors
15
+
16
+ ### Technical
17
+ - Consolidated create and update logic in a single afterChange hook
18
+ - Added comprehensive content change detection before syncing
19
+ - Enhanced logging for update sync operations
20
+ - Removed redundant beforeChange hook logic
21
+
22
+ ## [0.16.5] - 2025-07-29
23
+
24
+ ### Breaking Changes
25
+ - **Field Renaming** - Renamed `status` field to `sendStatus` throughout the codebase
26
+ - This avoids confusion with Payload's built-in `_status` field (draft/published)
27
+ - Database field is now `sendStatus` for email send status (draft, scheduled, sending, sent, etc.)
28
+ - All references in providers, endpoints, and types have been updated
29
+ - If you have existing broadcast data with a `status` field, you'll need to migrate it to `sendStatus`
30
+
31
+ ### Fixed
32
+ - **Update Sync** - Fixed issue where broadcast updates made in Payload weren't syncing to the Broadcast provider
33
+ - The update hook now correctly checks `sendStatus` instead of the non-existent `status` field
34
+ - Provider can now properly determine if a broadcast is editable based on its send status
35
+
36
+ ### Technical
37
+ - Updated `Broadcast` type interface to use `sendStatus` property
38
+ - Updated all provider implementations (Broadcast and Resend) to use `sendStatus`
39
+ - Updated send and schedule endpoints to set `sendStatus` field
40
+ - All TypeScript errors resolved
41
+
42
+ ## [0.16.4] - 2025-07-27
43
+
44
+ ### Added
45
+ - **Access Control for Broadcasts** - Added proper access control to the Broadcasts collection
46
+ - Public read access for all users
47
+ - Create, update, and delete operations require authenticated users
48
+ - Prevents unauthorized modifications to broadcast content
49
+ - Follows Payload's standard access control patterns
50
+
51
+ ### Improved
52
+ - **Enhanced Update Sync Debugging** - Added detailed logging for broadcast update synchronization
53
+ - Logs when update hooks are triggered with operation details
54
+ - Shows what fields are being synced to the provider
55
+ - Helps diagnose why updates might not be syncing
56
+ - Added info logging for skipped updates due to status restrictions
57
+ - **Clearer Field Naming** - Renamed `status` field to `sendStatus` to avoid confusion with Payload's `_status`
58
+ - Database field is now `sendStatus` (draft, scheduled, sending, sent, etc.)
59
+ - Payload's versioning field remains `_status` (draft, published)
60
+ - Added virtual `status` field for API backward compatibility
61
+ - Makes it clear which status controls email sending vs content publishing
62
+
1
63
  ## [0.16.3] - 2025-07-27
2
64
 
3
65
  ### Improved
@@ -291,9 +291,9 @@ var init_broadcast2 = __esm({
291
291
  async update(id, data) {
292
292
  try {
293
293
  const existing = await this.get(id);
294
- if (!this.canEditInStatus(existing.status)) {
294
+ if (!this.canEditInStatus(existing.sendStatus)) {
295
295
  throw new BroadcastProviderError(
296
- `Cannot update broadcast in status: ${existing.status}`,
296
+ `Cannot update broadcast in status: ${existing.sendStatus}`,
297
297
  "INVALID_STATUS" /* INVALID_STATUS */,
298
298
  this.name
299
299
  );
@@ -336,9 +336,9 @@ var init_broadcast2 = __esm({
336
336
  async delete(id) {
337
337
  try {
338
338
  const existing = await this.get(id);
339
- if (!this.canEditInStatus(existing.status)) {
339
+ if (!this.canEditInStatus(existing.sendStatus)) {
340
340
  throw new BroadcastProviderError(
341
- `Cannot delete broadcast in status: ${existing.status}`,
341
+ `Cannot delete broadcast in status: ${existing.sendStatus}`,
342
342
  "INVALID_STATUS" /* INVALID_STATUS */,
343
343
  this.name
344
344
  );
@@ -497,7 +497,7 @@ var init_broadcast2 = __esm({
497
497
  subject: broadcast.subject,
498
498
  preheader: broadcast.preheader,
499
499
  content: broadcast.body,
500
- status: this.mapBroadcastStatus(broadcast.status),
500
+ sendStatus: this.mapBroadcastStatus(broadcast.status),
501
501
  trackOpens: broadcast.track_opens,
502
502
  trackClicks: broadcast.track_clicks,
503
503
  replyTo: broadcast.reply_to,
@@ -1191,6 +1191,19 @@ var createBroadcastsCollection = (pluginConfig) => {
1191
1191
  const customizations = pluginConfig.customizations?.broadcasts;
1192
1192
  return {
1193
1193
  slug: "broadcasts",
1194
+ access: {
1195
+ read: () => true,
1196
+ // Public read access
1197
+ create: ({ req: { user } }) => {
1198
+ return Boolean(user);
1199
+ },
1200
+ update: ({ req: { user } }) => {
1201
+ return Boolean(user);
1202
+ },
1203
+ delete: ({ req: { user } }) => {
1204
+ return Boolean(user);
1205
+ }
1206
+ },
1194
1207
  versions: {
1195
1208
  drafts: {
1196
1209
  autosave: true,
@@ -1204,7 +1217,7 @@ var createBroadcastsCollection = (pluginConfig) => {
1204
1217
  admin: {
1205
1218
  useAsTitle: "subject",
1206
1219
  description: "Individual email campaigns sent to subscribers",
1207
- defaultColumns: ["subject", "_status", "status", "sentAt", "recipientCount"]
1220
+ defaultColumns: ["subject", "_status", "sendStatus", "sentAt", "recipientCount"]
1208
1221
  },
1209
1222
  fields: [
1210
1223
  {
@@ -1264,8 +1277,9 @@ var createBroadcastsCollection = (pluginConfig) => {
1264
1277
  ]
1265
1278
  },
1266
1279
  {
1267
- name: "status",
1280
+ name: "sendStatus",
1268
1281
  type: "select",
1282
+ label: "Send Status",
1269
1283
  required: true,
1270
1284
  defaultValue: "draft" /* DRAFT */,
1271
1285
  options: [
@@ -1279,6 +1293,7 @@ var createBroadcastsCollection = (pluginConfig) => {
1279
1293
  ],
1280
1294
  admin: {
1281
1295
  readOnly: true,
1296
+ description: "The status of the email send operation",
1282
1297
  components: {
1283
1298
  Cell: "payload-plugin-newsletter/components#StatusBadge"
1284
1299
  }
@@ -1335,7 +1350,7 @@ var createBroadcastsCollection = (pluginConfig) => {
1335
1350
  type: "group",
1336
1351
  admin: {
1337
1352
  readOnly: true,
1338
- condition: (data) => data?.status === "sent" /* SENT */
1353
+ condition: (data) => data?.sendStatus === "sent" /* SENT */
1339
1354
  },
1340
1355
  fields: [
1341
1356
  {
@@ -1394,7 +1409,7 @@ var createBroadcastsCollection = (pluginConfig) => {
1394
1409
  name: "scheduledAt",
1395
1410
  type: "date",
1396
1411
  admin: {
1397
- condition: (data) => data?.status === "scheduled" /* SCHEDULED */,
1412
+ condition: (data) => data?.sendStatus === "scheduled" /* SCHEDULED */,
1398
1413
  date: {
1399
1414
  displayFormat: "MMM d, yyyy h:mm a"
1400
1415
  }
@@ -1420,58 +1435,130 @@ var createBroadcastsCollection = (pluginConfig) => {
1420
1435
  }
1421
1436
  ],
1422
1437
  hooks: {
1423
- // Sync with provider on create
1438
+ // Sync with provider on create and update
1424
1439
  afterChange: [
1425
- async ({ doc, operation, req }) => {
1426
- if (!hasProviders || operation !== "create") return doc;
1427
- try {
1428
- const providerConfig = await getBroadcastConfig(req, pluginConfig);
1429
- if (!providerConfig || !providerConfig.token) {
1430
- req.payload.logger.error("Broadcast provider not configured in Newsletter Settings or environment variables");
1431
- return doc;
1432
- }
1433
- const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
1434
- const provider = new BroadcastApiProvider2(providerConfig);
1435
- const htmlContent = await convertToEmailSafeHtml(doc.contentSection?.content);
1436
- const providerBroadcast = await provider.create({
1437
- name: doc.subject,
1438
- // Use subject as name since we removed the name field
1439
- subject: doc.subject,
1440
- preheader: doc.contentSection?.preheader,
1441
- content: htmlContent,
1442
- trackOpens: doc.settings?.trackOpens,
1443
- trackClicks: doc.settings?.trackClicks,
1444
- replyTo: doc.settings?.replyTo || providerConfig.replyTo,
1445
- audienceIds: doc.audienceIds?.map((a) => a.audienceId)
1446
- });
1447
- await req.payload.update({
1448
- collection: "broadcasts",
1449
- id: doc.id,
1450
- data: {
1440
+ async ({ doc, operation, req, previousDoc }) => {
1441
+ if (!hasProviders) return doc;
1442
+ if (operation === "create") {
1443
+ try {
1444
+ const providerConfig = await getBroadcastConfig(req, pluginConfig);
1445
+ if (!providerConfig || !providerConfig.token) {
1446
+ req.payload.logger.error("Broadcast provider not configured in Newsletter Settings or environment variables");
1447
+ return doc;
1448
+ }
1449
+ const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
1450
+ const provider = new BroadcastApiProvider2(providerConfig);
1451
+ const htmlContent = await convertToEmailSafeHtml(doc.contentSection?.content);
1452
+ const providerBroadcast = await provider.create({
1453
+ name: doc.subject,
1454
+ // Use subject as name since we removed the name field
1455
+ subject: doc.subject,
1456
+ preheader: doc.contentSection?.preheader,
1457
+ content: htmlContent,
1458
+ trackOpens: doc.settings?.trackOpens,
1459
+ trackClicks: doc.settings?.trackClicks,
1460
+ replyTo: doc.settings?.replyTo || providerConfig.replyTo,
1461
+ audienceIds: doc.audienceIds?.map((a) => a.audienceId)
1462
+ });
1463
+ await req.payload.update({
1464
+ collection: "broadcasts",
1465
+ id: doc.id,
1466
+ data: {
1467
+ providerId: providerBroadcast.id,
1468
+ providerData: providerBroadcast.providerData
1469
+ },
1470
+ req
1471
+ });
1472
+ return {
1473
+ ...doc,
1451
1474
  providerId: providerBroadcast.id,
1452
1475
  providerData: providerBroadcast.providerData
1453
- },
1454
- req
1476
+ };
1477
+ } catch (error) {
1478
+ if (error instanceof Error) {
1479
+ req.payload.logger.error("Failed to create broadcast in provider:", {
1480
+ message: error.message,
1481
+ stack: error.stack,
1482
+ name: error.name,
1483
+ // If it's a BroadcastProviderError, it might have additional details
1484
+ ...error.details
1485
+ });
1486
+ } else {
1487
+ req.payload.logger.error("Failed to create broadcast in provider:", error);
1488
+ }
1489
+ return doc;
1490
+ }
1491
+ }
1492
+ if (operation === "update" && doc.providerId) {
1493
+ req.payload.logger.info("Broadcast afterChange update hook triggered", {
1494
+ operation,
1495
+ hasProviderId: !!doc.providerId,
1496
+ sendStatus: doc.sendStatus,
1497
+ publishStatus: doc._status
1455
1498
  });
1456
- return {
1457
- ...doc,
1458
- providerId: providerBroadcast.id,
1459
- providerData: providerBroadcast.providerData
1460
- };
1461
- } catch (error) {
1462
- if (error instanceof Error) {
1463
- req.payload.logger.error("Failed to create broadcast in provider:", {
1464
- message: error.message,
1465
- stack: error.stack,
1466
- name: error.name,
1467
- // If it's a BroadcastProviderError, it might have additional details
1468
- ...error.details
1469
- });
1470
- } else {
1471
- req.payload.logger.error("Failed to create broadcast in provider:", error);
1499
+ try {
1500
+ const providerConfig = await getBroadcastConfig(req, pluginConfig);
1501
+ if (!providerConfig || !providerConfig.token) {
1502
+ req.payload.logger.error("Broadcast provider not configured in Newsletter Settings or environment variables");
1503
+ return doc;
1504
+ }
1505
+ const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
1506
+ const provider = new BroadcastApiProvider2(providerConfig);
1507
+ const capabilities = provider.getCapabilities();
1508
+ const sendStatus = doc.sendStatus || "draft" /* DRAFT */;
1509
+ if (!capabilities.editableStatuses.includes(sendStatus)) {
1510
+ req.payload.logger.info(`Skipping sync for broadcast in status: ${sendStatus}`);
1511
+ return doc;
1512
+ }
1513
+ const contentChanged = doc.subject !== previousDoc?.subject || doc.contentSection?.preheader !== previousDoc?.contentSection?.preheader || JSON.stringify(doc.contentSection?.content) !== JSON.stringify(previousDoc?.contentSection?.content) || doc.settings?.trackOpens !== previousDoc?.settings?.trackOpens || doc.settings?.trackClicks !== previousDoc?.settings?.trackClicks || doc.settings?.replyTo !== previousDoc?.settings?.replyTo || JSON.stringify(doc.audienceIds) !== JSON.stringify(previousDoc?.audienceIds);
1514
+ if (contentChanged) {
1515
+ const updates = {};
1516
+ if (doc.subject !== previousDoc?.subject) {
1517
+ updates.name = doc.subject;
1518
+ updates.subject = doc.subject;
1519
+ }
1520
+ if (doc.contentSection?.preheader !== previousDoc?.contentSection?.preheader) {
1521
+ updates.preheader = doc.contentSection?.preheader;
1522
+ }
1523
+ if (JSON.stringify(doc.contentSection?.content) !== JSON.stringify(previousDoc?.contentSection?.content)) {
1524
+ updates.content = await convertToEmailSafeHtml(doc.contentSection?.content);
1525
+ }
1526
+ if (doc.settings?.trackOpens !== previousDoc?.settings?.trackOpens) {
1527
+ updates.trackOpens = doc.settings.trackOpens;
1528
+ }
1529
+ if (doc.settings?.trackClicks !== previousDoc?.settings?.trackClicks) {
1530
+ updates.trackClicks = doc.settings.trackClicks;
1531
+ }
1532
+ if (doc.settings?.replyTo !== previousDoc?.settings?.replyTo) {
1533
+ updates.replyTo = doc.settings.replyTo || providerConfig.replyTo;
1534
+ }
1535
+ if (JSON.stringify(doc.audienceIds) !== JSON.stringify(previousDoc?.audienceIds)) {
1536
+ updates.audienceIds = doc.audienceIds?.map((a) => a.audienceId);
1537
+ }
1538
+ req.payload.logger.info("Syncing broadcast updates to provider", {
1539
+ providerId: doc.providerId,
1540
+ updates
1541
+ });
1542
+ await provider.update(doc.providerId, updates);
1543
+ req.payload.logger.info(`Broadcast ${doc.id} synced to provider successfully`);
1544
+ } else {
1545
+ req.payload.logger.info("No content changes to sync to provider");
1546
+ }
1547
+ } catch (error) {
1548
+ if (error instanceof Error) {
1549
+ req.payload.logger.error("Failed to sync broadcast update to provider:", {
1550
+ message: error.message,
1551
+ stack: error.stack,
1552
+ name: error.name,
1553
+ // If it's a BroadcastProviderError, it might have additional details
1554
+ ...error.details
1555
+ });
1556
+ } else {
1557
+ req.payload.logger.error("Failed to sync broadcast update to provider:", error);
1558
+ }
1472
1559
  }
1473
- return doc;
1474
1560
  }
1561
+ return doc;
1475
1562
  },
1476
1563
  // Hook to send when published
1477
1564
  async ({ doc, operation, previousDoc, req }) => {
@@ -1479,7 +1566,7 @@ var createBroadcastsCollection = (pluginConfig) => {
1479
1566
  const wasUnpublished = !previousDoc?._status || previousDoc._status === "draft";
1480
1567
  const isNowPublished = doc._status === "published";
1481
1568
  if (wasUnpublished && isNowPublished && doc.providerId) {
1482
- if (doc.status === "sent" || doc.status === "sending") {
1569
+ if (doc.sendStatus === "sent" /* SENT */ || doc.sendStatus === "sending" /* SENDING */) {
1483
1570
  return doc;
1484
1571
  }
1485
1572
  try {
@@ -1498,7 +1585,7 @@ var createBroadcastsCollection = (pluginConfig) => {
1498
1585
  collection: "broadcasts",
1499
1586
  id: doc.id,
1500
1587
  data: {
1501
- status: "sending" /* SENDING */,
1588
+ sendStatus: "sending" /* SENDING */,
1502
1589
  sentAt: (/* @__PURE__ */ new Date()).toISOString()
1503
1590
  },
1504
1591
  req
@@ -1520,7 +1607,7 @@ var createBroadcastsCollection = (pluginConfig) => {
1520
1607
  collection: "broadcasts",
1521
1608
  id: doc.id,
1522
1609
  data: {
1523
- status: "failed" /* FAILED */
1610
+ sendStatus: "failed" /* FAILED */
1524
1611
  },
1525
1612
  req
1526
1613
  });
@@ -1529,62 +1616,8 @@ var createBroadcastsCollection = (pluginConfig) => {
1529
1616
  return doc;
1530
1617
  }
1531
1618
  ],
1532
- // Sync updates with provider
1533
- beforeChange: [
1534
- async ({ data, originalDoc, operation, req }) => {
1535
- if (!hasProviders || !originalDoc?.providerId || operation !== "update") return data;
1536
- try {
1537
- const providerConfig = await getBroadcastConfig(req, pluginConfig);
1538
- if (!providerConfig || !providerConfig.token) {
1539
- req.payload.logger.error("Broadcast provider not configured in Newsletter Settings or environment variables");
1540
- return data;
1541
- }
1542
- const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
1543
- const provider = new BroadcastApiProvider2(providerConfig);
1544
- const capabilities = provider.getCapabilities();
1545
- if (!capabilities.editableStatuses.includes(originalDoc.status)) {
1546
- return data;
1547
- }
1548
- const updates = {};
1549
- if (data.subject !== originalDoc.subject) {
1550
- updates.name = data.subject;
1551
- updates.subject = data.subject;
1552
- }
1553
- if (data.contentSection?.preheader !== originalDoc.contentSection?.preheader) updates.preheader = data.contentSection?.preheader;
1554
- if (data.contentSection?.content !== originalDoc.contentSection?.content) {
1555
- updates.content = await convertToEmailSafeHtml(data.contentSection?.content);
1556
- }
1557
- if (data.settings?.trackOpens !== originalDoc.settings?.trackOpens) {
1558
- updates.trackOpens = data.settings.trackOpens;
1559
- }
1560
- if (data.settings?.trackClicks !== originalDoc.settings?.trackClicks) {
1561
- updates.trackClicks = data.settings.trackClicks;
1562
- }
1563
- if (data.settings?.replyTo !== originalDoc.settings?.replyTo) {
1564
- updates.replyTo = data.settings.replyTo || providerConfig.replyTo;
1565
- }
1566
- if (JSON.stringify(data.audienceIds) !== JSON.stringify(originalDoc.audienceIds)) {
1567
- updates.audienceIds = data.audienceIds?.map((a) => a.audienceId);
1568
- }
1569
- if (Object.keys(updates).length > 0) {
1570
- await provider.update(originalDoc.providerId, updates);
1571
- }
1572
- } catch (error) {
1573
- if (error instanceof Error) {
1574
- req.payload.logger.error("Failed to update broadcast in provider:", {
1575
- message: error.message,
1576
- stack: error.stack,
1577
- name: error.name,
1578
- // If it's a BroadcastProviderError, it might have additional details
1579
- ...error.details
1580
- });
1581
- } else {
1582
- req.payload.logger.error("Failed to update broadcast in provider:", error);
1583
- }
1584
- }
1585
- return data;
1586
- }
1587
- ],
1619
+ // beforeChange hooks can be added here if needed
1620
+ beforeChange: [],
1588
1621
  // Handle deletion
1589
1622
  afterDelete: [
1590
1623
  async ({ doc, req }) => {
@@ -1598,7 +1631,7 @@ var createBroadcastsCollection = (pluginConfig) => {
1598
1631
  const { BroadcastApiProvider: BroadcastApiProvider2 } = await Promise.resolve().then(() => (init_broadcast2(), broadcast_exports));
1599
1632
  const provider = new BroadcastApiProvider2(providerConfig);
1600
1633
  const capabilities = provider.getCapabilities();
1601
- if (capabilities.editableStatuses.includes(doc.status)) {
1634
+ if (capabilities.editableStatuses.includes(doc.sendStatus)) {
1602
1635
  await provider.delete(doc.providerId);
1603
1636
  }
1604
1637
  } catch (error) {