payload-plugin-newsletter 0.4.5 → 0.6.1

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,6 +1,11 @@
1
- ## [0.4.5] - 2025-06-19
1
+ ## [0.6.1] - 2025-06-20
2
+
3
+ - fix: update Broadcast provider to use correct API endpoints
4
+
2
5
 
3
- - fix: add tsup build system for proper ESM/CJS dual package support
6
+ - fix: resolve ESLint error for unused variable in test mock
7
+ - fix: update tests to support newsletter settings as global
8
+ - feat: convert newsletter settings from collection to global
4
9
 
5
10
 
6
11
  All notable changes to this project will be documented in this file.
@@ -8,6 +13,28 @@ All notable changes to this project will be documented in this file.
8
13
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
9
14
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
10
15
 
16
+ ## [0.5.0] - 2025-06-20
17
+
18
+ ### Changed
19
+ - **BREAKING**: Changed newsletter settings from a collection back to a global configuration
20
+ - Settings are now accessed as a single global config instead of multiple documents
21
+ - Removed the "name" and "active" fields as they're no longer needed for globals
22
+ - Settings now appear as a single page in the admin UI instead of a list view
23
+ - Updated all code to use `payload.findGlobal()` instead of `payload.find()`
24
+
25
+ ### Migration Required
26
+ - Users with existing newsletter-settings collections will need to manually copy their active configuration to the new global settings
27
+ - After migration, the old newsletter-settings collection can be removed from the database
28
+
29
+ ### Fixed
30
+ - Resolved user confusion around having multiple settings documents when only one could be active
31
+ - Settings now follow the standard Payload pattern for configuration globals
32
+
33
+ ## [0.4.5] - 2025-06-19
34
+
35
+ ### Fixed
36
+ - Added tsup build system for proper ESM/CJS dual package support
37
+
11
38
  ## [0.4.4] - 2025-06-16
12
39
 
13
40
  ### Fixed
@@ -0,0 +1,74 @@
1
+ # ESM/CJS Module Resolution Fix Summary
2
+
3
+ ## Changes Made
4
+
5
+ ### 1. **Added tsup Build Tool**
6
+ - Added `tsup` v8.3.5 to devDependencies for proper ESM/CJS dual package support
7
+ - Created `tsup.config.ts` with configuration for building both ESM and CJS outputs
8
+
9
+ ### 2. **Updated package.json**
10
+ - Changed entry points from source files (`./src/`) to built files (`./dist/`)
11
+ - Added proper exports configuration with both ESM and CJS support:
12
+ - ESM files: `.js` extension with `.d.ts` types
13
+ - CJS files: `.cjs` extension with `.d.cts` types
14
+ - Updated build scripts to use tsup
15
+ - Added `module` field pointing to ESM entry
16
+
17
+ ### 3. **Updated tsconfig.json**
18
+ - Changed `moduleResolution` from "bundler" to "node" for better compatibility
19
+
20
+ ### 4. **Created Build Script**
21
+ - Added `build-and-commit.sh` for easy building and verification
22
+
23
+ ## File Structure After Build
24
+
25
+ ```
26
+ dist/
27
+ ├── index.js # ESM main entry
28
+ ├── index.cjs # CJS main entry
29
+ ├── index.d.ts # TypeScript definitions
30
+ ├── index.d.cts # CJS TypeScript definitions
31
+ ├── client.js # ESM client export
32
+ ├── client.cjs # CJS client export
33
+ ├── client.d.ts # Client TypeScript definitions
34
+ ├── client.d.cts # CJS Client TypeScript definitions
35
+ ├── types.js # ESM types export
36
+ ├── types.cjs # CJS types export
37
+ ├── types.d.ts # Types TypeScript definitions
38
+ ├── types.d.cts # CJS Types TypeScript definitions
39
+ ├── components.js # ESM components export
40
+ ├── components.cjs # CJS components export
41
+ ├── components.d.ts # Components TypeScript definitions
42
+ └── components.d.cts # CJS Components TypeScript definitions
43
+ ```
44
+
45
+ ## How to Build and Publish
46
+
47
+ 1. **Install dependencies**: `bun install`
48
+ 2. **Build the package**: `bun run build`
49
+ 3. **Test locally**: Link the package to test in your project
50
+ 4. **Publish**: `npm publish` (dist files will be included automatically)
51
+
52
+ ## Benefits
53
+
54
+ 1. **Proper ESM Support**: Next.js apps using `"type": "module"` can now import the package without issues
55
+ 2. **CJS Compatibility**: Still works with CommonJS projects
56
+ 3. **TypeScript Support**: Proper type definitions for both module systems
57
+ 4. **Clean Exports**: Clear separation of server/client code with proper exports
58
+ 5. **Future Proof**: Ready for the ESM-first ecosystem
59
+
60
+ ## Testing the Fix
61
+
62
+ To test in your ContentQuant project:
63
+ 1. Build the plugin: `bun run build`
64
+ 2. Link locally: `npm link` in plugin directory
65
+ 3. Link in project: `npm link payload-plugin-newsletter` in ContentQuant
66
+ 4. Import and use normally
67
+
68
+ ## Next Steps
69
+
70
+ 1. Run `./build-and-commit.sh` to build the package
71
+ 2. Test the built package locally
72
+ 3. Commit changes with message: "fix: add tsup build system for proper ESM/CJS dual package support"
73
+ 4. Push to repository
74
+ 5. Publish new version to npm
package/dist/index.cjs CHANGED
@@ -357,40 +357,17 @@ var createSubscribersCollection = (pluginConfig) => {
357
357
  return subscribersCollection;
358
358
  };
359
359
 
360
- // src/collections/NewsletterSettings.ts
361
- var createNewsletterSettingsCollection = (pluginConfig) => {
360
+ // src/globals/NewsletterSettings.ts
361
+ var createNewsletterSettingsGlobal = (pluginConfig) => {
362
362
  const slug = pluginConfig.settingsSlug || "newsletter-settings";
363
363
  return {
364
364
  slug,
365
- labels: {
366
- singular: "Newsletter Setting",
367
- plural: "Newsletter Settings"
368
- },
365
+ label: "Newsletter Settings",
369
366
  admin: {
370
- useAsTitle: "name",
371
- defaultColumns: ["name", "provider", "active", "updatedAt"],
372
367
  group: "Newsletter",
373
368
  description: "Configure email provider settings and templates"
374
369
  },
375
370
  fields: [
376
- {
377
- name: "name",
378
- type: "text",
379
- label: "Configuration Name",
380
- required: true,
381
- admin: {
382
- description: 'A descriptive name for this configuration (e.g., "Production", "Development", "Marketing Emails")'
383
- }
384
- },
385
- {
386
- name: "active",
387
- type: "checkbox",
388
- label: "Active",
389
- defaultValue: false,
390
- admin: {
391
- description: "Only one configuration can be active at a time"
392
- }
393
- },
394
371
  {
395
372
  type: "tabs",
396
373
  tabs: [
@@ -650,53 +627,16 @@ var createNewsletterSettingsCollection = (pluginConfig) => {
650
627
  ],
651
628
  hooks: {
652
629
  beforeChange: [
653
- async ({ data, req, operation }) => {
630
+ async ({ data, req }) => {
654
631
  if (!req.user || req.user.collection !== "users") {
655
632
  throw new Error("Only administrators can modify newsletter settings");
656
633
  }
657
- if (data?.active && operation !== "create") {
658
- await req.payload.update({
659
- collection: slug,
660
- where: {
661
- id: {
662
- not_equals: data.id
663
- }
664
- },
665
- data: {
666
- active: false
667
- }
668
- // Keep overrideAccess: true for admin operations after verification
669
- });
670
- }
671
- if (operation === "create" && data?.active) {
672
- const existingActive = await req.payload.find({
673
- collection: slug,
674
- where: {
675
- active: {
676
- equals: true
677
- }
678
- }
679
- // Keep overrideAccess: true for admin operations
680
- });
681
- if (existingActive.docs.length > 0) {
682
- for (const doc of existingActive.docs) {
683
- await req.payload.update({
684
- collection: slug,
685
- id: doc.id,
686
- data: {
687
- active: false
688
- }
689
- // Keep overrideAccess: true for admin operations
690
- });
691
- }
692
- }
693
- }
694
634
  return data;
695
635
  }
696
636
  ],
697
637
  afterChange: [
698
638
  async ({ doc, req }) => {
699
- if (req.payload.newsletterEmailService && doc.active) {
639
+ if (req.payload.newsletterEmailService) {
700
640
  try {
701
641
  console.warn("Newsletter settings updated, reinitializing service...");
702
642
  } catch {
@@ -709,11 +649,8 @@ var createNewsletterSettingsCollection = (pluginConfig) => {
709
649
  access: {
710
650
  read: () => true,
711
651
  // Settings can be read publicly for validation
712
- create: adminOnly(pluginConfig),
713
- update: adminOnly(pluginConfig),
714
- delete: adminOnly(pluginConfig)
715
- },
716
- timestamps: true
652
+ update: adminOnly(pluginConfig)
653
+ }
717
654
  };
718
655
  };
719
656
 
@@ -900,20 +837,22 @@ var BroadcastProvider = class {
900
837
  }
901
838
  async addContact(contact) {
902
839
  try {
903
- const response = await fetch(`${this.apiUrl}/api/v1/contacts`, {
840
+ const [firstName, ...lastNameParts] = (contact.name || "").split(" ");
841
+ const lastName = lastNameParts.join(" ");
842
+ const response = await fetch(`${this.apiUrl}/api/v1/subscribers.json`, {
904
843
  method: "POST",
905
844
  headers: {
906
845
  "Authorization": `Bearer ${this.token}`,
907
846
  "Content-Type": "application/json"
908
847
  },
909
848
  body: JSON.stringify({
910
- email: contact.email,
911
- name: contact.name,
912
- status: contact.subscriptionStatus === "active" ? "subscribed" : "unsubscribed",
913
- metadata: {
914
- locale: contact.locale,
915
- source: contact.source,
916
- ...contact.utmParameters
849
+ subscriber: {
850
+ email: contact.email,
851
+ first_name: firstName || void 0,
852
+ last_name: lastName || void 0,
853
+ tags: [`lang:${contact.locale || "en"}`],
854
+ is_active: contact.subscriptionStatus === "active",
855
+ source: contact.source
917
856
  }
918
857
  })
919
858
  });
@@ -932,7 +871,7 @@ var BroadcastProvider = class {
932
871
  async updateContact(contact) {
933
872
  try {
934
873
  const searchResponse = await fetch(
935
- `${this.apiUrl}/api/v1/contacts?email=${encodeURIComponent(contact.email)}`,
874
+ `${this.apiUrl}/api/v1/subscribers/find.json?email=${encodeURIComponent(contact.email)}`,
936
875
  {
937
876
  headers: {
938
877
  "Authorization": `Bearer ${this.token}`
@@ -943,26 +882,27 @@ var BroadcastProvider = class {
943
882
  await this.addContact(contact);
944
883
  return;
945
884
  }
946
- const contacts = await searchResponse.json();
947
- const existingContact = contacts.data?.[0];
948
- if (!existingContact) {
885
+ const existingContact = await searchResponse.json();
886
+ if (!existingContact || !existingContact.id) {
949
887
  await this.addContact(contact);
950
888
  return;
951
889
  }
952
- const response = await fetch(`${this.apiUrl}/api/v1/contacts/${existingContact.id}`, {
953
- method: "PUT",
890
+ const [firstName, ...lastNameParts] = (contact.name || "").split(" ");
891
+ const lastName = lastNameParts.join(" ");
892
+ const response = await fetch(`${this.apiUrl}/api/v1/subscribers.json`, {
893
+ method: "PATCH",
954
894
  headers: {
955
895
  "Authorization": `Bearer ${this.token}`,
956
896
  "Content-Type": "application/json"
957
897
  },
958
898
  body: JSON.stringify({
959
899
  email: contact.email,
960
- name: contact.name,
961
- status: contact.subscriptionStatus === "active" ? "subscribed" : "unsubscribed",
962
- metadata: {
963
- locale: contact.locale,
964
- source: contact.source,
965
- ...contact.utmParameters
900
+ subscriber: {
901
+ first_name: firstName || void 0,
902
+ last_name: lastName || void 0,
903
+ tags: [`lang:${contact.locale || "en"}`],
904
+ is_active: contact.subscriptionStatus === "active",
905
+ source: contact.source
966
906
  }
967
907
  })
968
908
  });
@@ -981,7 +921,7 @@ var BroadcastProvider = class {
981
921
  async removeContact(email) {
982
922
  try {
983
923
  const searchResponse = await fetch(
984
- `${this.apiUrl}/api/v1/contacts?email=${encodeURIComponent(email)}`,
924
+ `${this.apiUrl}/api/v1/subscribers/find.json?email=${encodeURIComponent(email)}`,
985
925
  {
986
926
  headers: {
987
927
  "Authorization": `Bearer ${this.token}`
@@ -991,16 +931,17 @@ var BroadcastProvider = class {
991
931
  if (!searchResponse.ok) {
992
932
  return;
993
933
  }
994
- const contacts = await searchResponse.json();
995
- const contact = contacts.data?.[0];
996
- if (!contact) {
934
+ const contact = await searchResponse.json();
935
+ if (!contact || !contact.id) {
997
936
  return;
998
937
  }
999
- const response = await fetch(`${this.apiUrl}/api/v1/contacts/${contact.id}`, {
1000
- method: "DELETE",
938
+ const response = await fetch(`${this.apiUrl}/api/v1/subscribers/deactivate.json`, {
939
+ method: "POST",
1001
940
  headers: {
1002
- "Authorization": `Bearer ${this.token}`
1003
- }
941
+ "Authorization": `Bearer ${this.token}`,
942
+ "Content-Type": "application/json"
943
+ },
944
+ body: JSON.stringify({ email })
1004
945
  });
1005
946
  if (!response.ok) {
1006
947
  const error = await response.text();
@@ -1193,18 +1134,11 @@ var createSubscribeEndpoint = (config) => {
1193
1134
  errors: validation.errors
1194
1135
  });
1195
1136
  }
1196
- const settingsResult = await req.payload.find({
1197
- collection: config.settingsSlug || "newsletter-settings",
1198
- where: {
1199
- active: {
1200
- equals: true
1201
- }
1202
- },
1203
- limit: 1,
1137
+ const settings = await req.payload.findGlobal({
1138
+ slug: config.settingsSlug || "newsletter-settings",
1204
1139
  overrideAccess: false
1205
1140
  // No user context for public endpoint
1206
1141
  });
1207
- const settings = settingsResult.docs[0];
1208
1142
  const allowedDomains = settings?.subscriptionSettings?.allowedDomains?.map((d) => d.domain) || [];
1209
1143
  if (!isDomainAllowed(trimmedEmail, allowedDomains)) {
1210
1144
  return res.status(400).json({
@@ -1909,8 +1843,8 @@ var newsletterPlugin = (pluginConfig) => (incomingConfig) => {
1909
1843
  return incomingConfig;
1910
1844
  }
1911
1845
  const subscribersCollection = createSubscribersCollection(config);
1912
- const settingsCollection = createNewsletterSettingsCollection(config);
1913
- let collections = [...incomingConfig.collections || [], subscribersCollection, settingsCollection];
1846
+ const settingsGlobal = createNewsletterSettingsGlobal(config);
1847
+ let collections = [...incomingConfig.collections || [], subscribersCollection];
1914
1848
  if (config.features?.newsletterScheduling?.enabled) {
1915
1849
  const targetCollections = config.features.newsletterScheduling.collections || "articles";
1916
1850
  const collectionsToExtend = Array.isArray(targetCollections) ? targetCollections : [targetCollections];
@@ -1933,7 +1867,8 @@ var newsletterPlugin = (pluginConfig) => (incomingConfig) => {
1933
1867
  ...incomingConfig,
1934
1868
  collections,
1935
1869
  globals: [
1936
- ...incomingConfig.globals || []
1870
+ ...incomingConfig.globals || [],
1871
+ settingsGlobal
1937
1872
  ],
1938
1873
  endpoints: [
1939
1874
  ...incomingConfig.endpoints || [],
@@ -1941,16 +1876,9 @@ var newsletterPlugin = (pluginConfig) => (incomingConfig) => {
1941
1876
  ],
1942
1877
  onInit: async (payload) => {
1943
1878
  try {
1944
- const settingsResult = await payload.find({
1945
- collection: config.settingsSlug || "newsletter-settings",
1946
- where: {
1947
- active: {
1948
- equals: true
1949
- }
1950
- },
1951
- limit: 1
1879
+ const settings = await payload.findGlobal({
1880
+ slug: config.settingsSlug || "newsletter-settings"
1952
1881
  });
1953
- const settings = settingsResult.docs[0];
1954
1882
  let emailServiceConfig;
1955
1883
  if (settings) {
1956
1884
  emailServiceConfig = {