@stacksjs/defaults 0.70.356 → 0.70.358

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.
Files changed (73) hide show
  1. package/ai/skills/stacks-browse/SKILL.md +5 -1
  2. package/ai/skills/stacks-browse/scripts/browse.ts +61 -6
  3. package/ai/skills/stacks-orm/SKILL.md +6 -0
  4. package/app/Actions/Dashboard/Actions/GetActions.ts +15 -9
  5. package/app/Actions/Dashboard/Analytics/EventStoreAction.ts +21 -9
  6. package/app/Actions/Dashboard/Commerce/ProductUnitDefaultAction.ts +12 -3
  7. package/app/Actions/Dashboard/Commerce/TaxRateDefaultAction.ts +12 -3
  8. package/app/Actions/Dashboard/Content/AuthorDestroyAction.ts +23 -9
  9. package/app/Actions/Dashboard/Content/AuthorIndexAction.ts +32 -26
  10. package/app/Actions/Dashboard/Content/AuthorStoreAction.ts +44 -24
  11. package/app/Actions/Dashboard/Content/AuthorUpdateAction.ts +48 -26
  12. package/app/Actions/Dashboard/Content/CategoryDestroyAction.ts +27 -8
  13. package/app/Actions/Dashboard/Content/CategoryIndexAction.ts +21 -15
  14. package/app/Actions/Dashboard/Content/CategoryStoreAction.ts +47 -29
  15. package/app/Actions/Dashboard/Content/CommentDestroyAction.ts +17 -4
  16. package/app/Actions/Dashboard/Content/CommentIndexAction.ts +23 -17
  17. package/app/Actions/Dashboard/Content/CommentUpdateAction.ts +30 -12
  18. package/app/Actions/Dashboard/Content/ContentDashboardAction.ts +43 -37
  19. package/app/Actions/Dashboard/Content/PageDestroyAction.ts +17 -4
  20. package/app/Actions/Dashboard/Content/PageIndexAction.ts +23 -17
  21. package/app/Actions/Dashboard/Content/PageStoreAction.ts +33 -18
  22. package/app/Actions/Dashboard/Content/PageUpdateAction.ts +40 -22
  23. package/app/Actions/Dashboard/Content/PostDestroyAction.ts +24 -11
  24. package/app/Actions/Dashboard/Content/PostIndexAction.ts +55 -49
  25. package/app/Actions/Dashboard/Content/PostStoreAction.ts +43 -33
  26. package/app/Actions/Dashboard/Content/PostUpdateAction.ts +46 -34
  27. package/app/Actions/Dashboard/Content/TagDestroyAction.ts +27 -8
  28. package/app/Actions/Dashboard/Content/TagIndexAction.ts +31 -25
  29. package/app/Actions/Dashboard/Content/TagStoreAction.ts +44 -23
  30. package/app/Actions/Dashboard/Content/content-input.ts +4 -4
  31. package/app/Actions/Dashboard/Content/post-input.ts +60 -8
  32. package/app/Actions/Dashboard/DashboardHealthAction.ts +34 -14
  33. package/app/Actions/Dashboard/DashboardHomeAction.test.ts +1 -1
  34. package/app/Actions/Dashboard/DashboardHomeAction.ts +31 -4
  35. package/app/Actions/Dashboard/DashboardStatsAction.ts +6 -1
  36. package/app/Actions/Dashboard/Email/InboxSendAction.ts +14 -7
  37. package/app/Actions/Dashboard/Infrastructure/CommandIndexAction.ts +33 -27
  38. package/app/Actions/Dashboard/Infrastructure/EnvironmentIndexAction.ts +10 -4
  39. package/app/Actions/Dashboard/Infrastructure/EnvironmentUpdateAction.ts +8 -1
  40. package/app/Actions/Dashboard/Infrastructure/RequestIndexAction.ts +24 -18
  41. package/app/Actions/Dashboard/Infrastructure/ServerIndexAction.ts +15 -9
  42. package/app/Actions/Dashboard/Infrastructure/ServerShowAction.ts +9 -2
  43. package/app/Actions/Dashboard/Marketing/CampaignCancelAction.ts +23 -5
  44. package/app/Actions/Dashboard/Marketing/CampaignDestroyAction.ts +26 -17
  45. package/app/Actions/Dashboard/Marketing/CampaignIndexAction.ts +49 -43
  46. package/app/Actions/Dashboard/Marketing/CampaignScheduleAction.ts +31 -25
  47. package/app/Actions/Dashboard/Marketing/CampaignSendAction.ts +28 -22
  48. package/app/Actions/Dashboard/Marketing/CampaignStoreAction.ts +15 -10
  49. package/app/Actions/Dashboard/Marketing/CampaignUpdateAction.ts +19 -12
  50. package/app/Actions/Dashboard/Marketing/ListDestroyAction.ts +34 -25
  51. package/app/Actions/Dashboard/Marketing/ListIndexAction.ts +32 -26
  52. package/app/Actions/Dashboard/Marketing/ListStoreAction.ts +31 -18
  53. package/app/Actions/Dashboard/Marketing/ListUpdateAction.ts +35 -19
  54. package/app/Actions/Dashboard/Marketing/SocialPostDestroyAction.ts +15 -5
  55. package/app/Actions/Dashboard/Marketing/SocialPostIndexAction.ts +11 -5
  56. package/app/Actions/Dashboard/Marketing/SocialPostStoreAction.ts +19 -13
  57. package/app/Actions/Dashboard/Marketing/SocialPostUpdateAction.ts +22 -12
  58. package/app/Actions/Dashboard/Marketing/campaign-delivery.ts +34 -0
  59. package/app/Actions/Dashboard/Marketing/marketing-list-records.test.ts +10 -0
  60. package/app/Actions/Dashboard/Marketing/marketing-list-records.ts +10 -0
  61. package/app/Actions/Dashboard/Marketing/marketing-response.ts +28 -0
  62. package/app/Actions/Dashboard/Marketing/social-post-records.test.ts +13 -0
  63. package/app/Actions/Dashboard/Marketing/social-post-records.ts +22 -4
  64. package/app/Actions/Dashboard/Models/GetModels.ts +14 -7
  65. package/app/Actions/Dashboard/Models/GetSubscriberCount.ts +8 -1
  66. package/app/Actions/Dashboard/Models/GetUserCount.ts +8 -1
  67. package/app/Actions/Dashboard/Releases/ReleaseIndexAction.ts +11 -5
  68. package/app/Actions/Dashboard/Settings/MailSettingsGetAction.ts +10 -4
  69. package/app/Actions/Dashboard/Settings/MailSettingsUpdateAction.ts +8 -1
  70. package/ide/vscode/package.json +1 -1
  71. package/package.json +1 -1
  72. package/resources/components/Dashboard/UI/Modal.stx +60 -10
  73. package/views/dashboard/stores/auth.ts +18 -2
@@ -3,6 +3,7 @@ import { Action } from '@stacksjs/actions'
3
3
  import { response } from '@stacksjs/router'
4
4
  import { tsCloud } from '~/config/cloud'
5
5
  import { getDashboardCloudSnapshot } from '../Cloud/cloud-overview'
6
+ import { dashboardOperationalError } from '../dashboard-response'
6
7
  import { resolveDashboardServer } from './server-detail'
7
8
 
8
9
  export default new Action({
@@ -12,10 +13,16 @@ export default new Action({
12
13
  apiResponse: true,
13
14
  async handle(request: RequestInstance) {
14
15
  const identifier = request.getParam('id')
15
- if (!identifier)
16
+ if (!identifier || !/^[A-Za-z0-9:_-]{1,160}$/.test(identifier))
16
17
  return response.json({ error: 'A server identifier is required.' }, 400)
17
18
 
18
- const snapshot = await getDashboardCloudSnapshot(tsCloud)
19
+ let snapshot
20
+ try {
21
+ snapshot = await getDashboardCloudSnapshot(tsCloud)
22
+ }
23
+ catch (error) {
24
+ return dashboardOperationalError(error, 'Server state could not be loaded.', 'ServerShowAction')
25
+ }
19
26
  const detail = resolveDashboardServer(snapshot, identifier)
20
27
  if (!detail)
21
28
  return response.json({ error: 'Server state was not found.' }, 404)
@@ -1,9 +1,11 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
- import { campaigns } from '@stacksjs/newsletter'
3
+ import { campaigns, CampaignStateConflictError } from '@stacksjs/newsletter'
4
4
  import { Campaign } from '@stacksjs/orm'
5
5
  import { response } from '@stacksjs/router'
6
+ import { dashboardOperationalError } from '../dashboard-response'
6
7
  import { validateCampaignDelivery } from './campaign-delivery'
8
+ import { marketingRecordId } from './marketing-response'
7
9
 
8
10
  export default new Action({
9
11
  name: 'CampaignCancelAction',
@@ -11,8 +13,17 @@ export default new Action({
11
13
  method: 'POST',
12
14
 
13
15
  async handle(request: RequestInstance) {
14
- const id = Number(request.getParam('id'))
15
- const campaign = await Campaign.find(id)
16
+ const id = marketingRecordId(request)
17
+ if (!id)
18
+ return response.json({ message: 'A valid campaign id is required.' }, 400)
19
+
20
+ let campaign
21
+ try {
22
+ campaign = await Campaign.find(id)
23
+ }
24
+ catch (error) {
25
+ return dashboardOperationalError(error, 'Campaign could not be loaded.', 'CampaignCancelAction.read')
26
+ }
16
27
  if (!campaign)
17
28
  return response.json({ message: 'Campaign not found.' }, 404)
18
29
 
@@ -20,7 +31,14 @@ export default new Action({
20
31
  if (validationError)
21
32
  return response.json({ message: validationError }, 422)
22
33
 
23
- await campaigns.cancel(id)
24
- return response.json({ id, status: 'cancelled' })
34
+ try {
35
+ await campaigns.cancel(id)
36
+ return response.json({ id, status: 'cancelled' })
37
+ }
38
+ catch (error) {
39
+ if (error instanceof CampaignStateConflictError)
40
+ return response.json({ message: 'Campaign delivery state changed. Refresh and try again.' }, 409)
41
+ return dashboardOperationalError(error, 'Campaign could not be cancelled.', 'CampaignCancelAction.cancel', 500)
42
+ }
25
43
  },
26
44
  })
@@ -3,6 +3,7 @@ import { Action } from '@stacksjs/actions'
3
3
  import { db } from '@stacksjs/database'
4
4
  import { Campaign } from '@stacksjs/orm'
5
5
  import { response } from '@stacksjs/router'
6
+ import { marketingModelError, marketingRecordId } from './marketing-response'
6
7
 
7
8
  export default new Action({
8
9
  name: 'CampaignDestroyAction',
@@ -10,24 +11,32 @@ export default new Action({
10
11
  method: 'DELETE',
11
12
 
12
13
  async handle(request: RequestInstance) {
13
- const id = Number(request.getParam('id'))
14
- const campaign = await Campaign.find(id)
15
- if (!campaign)
16
- return response.json({ message: 'Campaign not found.' }, 404)
14
+ const id = marketingRecordId(request)
15
+ if (!id)
16
+ return response.json({ message: 'A valid campaign id is required.' }, 400)
17
17
 
18
- const status = String(campaign.get('status') || '')
19
- const sendRow = await db
20
- .selectFrom('campaign_sends')
21
- .select(db.fn.count('id').as('count'))
22
- .where('campaign_id', '=', id)
23
- .executeTakeFirst()
24
- if (Number(sendRow?.count || 0) > 0 || ['scheduled', 'sending', 'sent'].includes(status)) {
25
- return response.json({
26
- message: 'Scheduled campaigns and campaigns with delivery history cannot be deleted.',
27
- }, 409)
28
- }
18
+ try {
19
+ const campaign = await Campaign.find(id)
20
+ if (!campaign)
21
+ return response.json({ message: 'Campaign not found.' }, 404)
22
+
23
+ const status = String(campaign.get('status') || '')
24
+ const sendRow = await db
25
+ .selectFrom('campaign_sends')
26
+ .select(db.fn.count('id').as('count'))
27
+ .where('campaign_id', '=', id)
28
+ .executeTakeFirst()
29
+ if (Number(sendRow?.count || 0) > 0 || ['scheduled', 'sending', 'sent'].includes(status)) {
30
+ return response.json({
31
+ message: 'Scheduled campaigns and campaigns with delivery history cannot be deleted.',
32
+ }, 409)
33
+ }
29
34
 
30
- await campaign.delete()
31
- return response.noContent()
35
+ await campaign.delete()
36
+ return response.noContent()
37
+ }
38
+ catch (error) {
39
+ return marketingModelError(error, 'Campaign could not be deleted.', 'CampaignDestroyAction')
40
+ }
32
41
  },
33
42
  })
@@ -2,6 +2,7 @@ import { Action } from '@stacksjs/actions'
2
2
  import { config } from '@stacksjs/config'
3
3
  import { db } from '@stacksjs/database'
4
4
  import { Campaign, EmailList } from '@stacksjs/orm'
5
+ import { dashboardOperationalError } from '../dashboard-response'
5
6
  import { normalizeCampaigns } from './campaign-records'
6
7
 
7
8
  export default new Action({
@@ -11,49 +12,54 @@ export default new Action({
11
12
  apiResponse: true,
12
13
 
13
14
  async handle() {
14
- const [
15
- campaigns,
16
- lists,
17
- membershipRows,
18
- sendRows,
19
- openedRows,
20
- clickedRows,
21
- ] = await Promise.all([
22
- Campaign.orderByDesc('id').limit(500).get(),
23
- EmailList.orderBy('name', 'asc').get(),
24
- db
25
- .selectFrom('email_list_subscribers')
26
- .select(['email_list_id', db.fn.count('id').as('count')])
27
- .where('status', '=', 'subscribed')
28
- .groupBy('email_list_id')
29
- .execute(),
30
- db
31
- .selectFrom('campaign_sends')
32
- .select(['campaign_id', 'status', db.fn.count('id').as('count')])
33
- .groupBy(['campaign_id', 'status'])
34
- .execute(),
35
- db
36
- .selectFrom('campaign_sends')
37
- .select(['campaign_id', db.fn.count('id').as('count')])
38
- .whereNotNull('opened_at')
39
- .groupBy('campaign_id')
40
- .execute(),
41
- db
42
- .selectFrom('campaign_sends')
43
- .select(['campaign_id', db.fn.count('id').as('count')])
44
- .whereNotNull('clicked_at')
45
- .groupBy('campaign_id')
46
- .execute(),
47
- ])
15
+ try {
16
+ const [
17
+ campaigns,
18
+ lists,
19
+ membershipRows,
20
+ sendRows,
21
+ openedRows,
22
+ clickedRows,
23
+ ] = await Promise.all([
24
+ Campaign.orderByDesc('id').limit(500).get(),
25
+ EmailList.orderBy('name', 'asc').get(),
26
+ db
27
+ .selectFrom('email_list_subscribers')
28
+ .select(['email_list_id', db.fn.count('id').as('count')])
29
+ .where('status', '=', 'subscribed')
30
+ .groupBy('email_list_id')
31
+ .execute(),
32
+ db
33
+ .selectFrom('campaign_sends')
34
+ .select(['campaign_id', 'status', db.fn.count('id').as('count')])
35
+ .groupBy(['campaign_id', 'status'])
36
+ .execute(),
37
+ db
38
+ .selectFrom('campaign_sends')
39
+ .select(['campaign_id', db.fn.count('id').as('count')])
40
+ .whereNotNull('opened_at')
41
+ .groupBy('campaign_id')
42
+ .execute(),
43
+ db
44
+ .selectFrom('campaign_sends')
45
+ .select(['campaign_id', db.fn.count('id').as('count')])
46
+ .whereNotNull('clicked_at')
47
+ .groupBy('campaign_id')
48
+ .execute(),
49
+ ])
48
50
 
49
- return normalizeCampaigns(
50
- campaigns,
51
- lists,
52
- membershipRows,
53
- sendRows,
54
- openedRows,
55
- clickedRows,
56
- String((config as any).commerce?.currency || 'USD').toUpperCase(),
57
- )
51
+ return normalizeCampaigns(
52
+ campaigns,
53
+ lists,
54
+ membershipRows,
55
+ sendRows,
56
+ openedRows,
57
+ clickedRows,
58
+ String((config as any).commerce?.currency || 'USD').toUpperCase(),
59
+ )
60
+ }
61
+ catch (error) {
62
+ return dashboardOperationalError(error, 'Campaigns could not be loaded.', 'CampaignIndexAction')
63
+ }
58
64
  },
59
65
  })
@@ -1,13 +1,14 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
- import { db } from '@stacksjs/database'
4
- import { campaigns } from '@stacksjs/newsletter'
5
- import { Campaign, EmailList } from '@stacksjs/orm'
3
+ import { campaigns, CampaignStateConflictError } from '@stacksjs/newsletter'
6
4
  import { response } from '@stacksjs/router'
5
+ import { dashboardOperationalError } from '../dashboard-response'
7
6
  import {
8
7
  campaignScheduleIso,
8
+ loadCampaignDeliveryContext,
9
9
  validateCampaignDelivery,
10
10
  } from './campaign-delivery'
11
+ import { marketingRecordId } from './marketing-response'
11
12
 
12
13
  export default new Action({
13
14
  name: 'CampaignScheduleAction',
@@ -15,35 +16,40 @@ export default new Action({
15
16
  method: 'POST',
16
17
 
17
18
  async handle(request: RequestInstance) {
18
- const id = Number(request.getParam('id'))
19
- const campaign = await Campaign.find(id)
20
- if (!campaign)
19
+ const id = marketingRecordId(request)
20
+ if (!id)
21
+ return response.json({ message: 'A valid campaign id is required.' }, 400)
22
+ const schedule = campaignScheduleIso((await request.all()).scheduledAt)
23
+ if (schedule.error)
24
+ return response.json({ message: schedule.error }, 422)
25
+
26
+ let context
27
+ try {
28
+ context = await loadCampaignDeliveryContext(id)
29
+ }
30
+ catch (error) {
31
+ return dashboardOperationalError(error, 'Campaign delivery prerequisites could not be loaded.', 'CampaignScheduleAction.prerequisites')
32
+ }
33
+ if (!context.campaign)
21
34
  return response.json({ message: 'Campaign not found.' }, 404)
22
35
 
23
- const listId = Number(campaign.get('email_list_id') || 0)
24
- const list = listId ? await EmailList.find(listId) : null
25
- const memberRow = listId
26
- ? await db
27
- .selectFrom('email_list_subscribers')
28
- .select(db.fn.count('id').as('count'))
29
- .where('email_list_id', '=', listId)
30
- .where('status', '=', 'subscribed')
31
- .executeTakeFirst()
32
- : null
33
36
  const validationError = validateCampaignDelivery(
34
- campaign,
37
+ context.campaign,
35
38
  'schedule',
36
- Number(memberRow?.count || 0),
37
- String(list?.get('status') || ''),
39
+ context.activeMembers,
40
+ context.listStatus,
38
41
  )
39
42
  if (validationError)
40
43
  return response.json({ message: validationError }, 422)
41
44
 
42
- const schedule = campaignScheduleIso((await request.all()).scheduledAt)
43
- if (schedule.error)
44
- return response.json({ message: schedule.error }, 422)
45
-
46
- await campaigns.schedule(id, schedule.value)
47
- return response.json({ id, status: 'scheduled', scheduledAt: schedule.value }, 202)
45
+ try {
46
+ await campaigns.schedule(id, schedule.value)
47
+ return response.json({ id, status: 'scheduled', scheduledAt: schedule.value }, 202)
48
+ }
49
+ catch (error) {
50
+ if (error instanceof CampaignStateConflictError)
51
+ return response.json({ message: 'Campaign delivery state changed. Refresh and try again.' }, 409)
52
+ return dashboardOperationalError(error, 'Campaign could not be scheduled.', 'CampaignScheduleAction.queue', 500)
53
+ }
48
54
  },
49
55
  })
@@ -1,10 +1,10 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
- import { db } from '@stacksjs/database'
4
- import { campaigns } from '@stacksjs/newsletter'
5
- import { Campaign, EmailList } from '@stacksjs/orm'
3
+ import { campaigns, CampaignStateConflictError } from '@stacksjs/newsletter'
6
4
  import { response } from '@stacksjs/router'
7
- import { validateCampaignDelivery } from './campaign-delivery'
5
+ import { dashboardOperationalError } from '../dashboard-response'
6
+ import { loadCampaignDeliveryContext, validateCampaignDelivery } from './campaign-delivery'
7
+ import { marketingRecordId } from './marketing-response'
8
8
 
9
9
  export default new Action({
10
10
  name: 'CampaignSendAction',
@@ -12,31 +12,37 @@ export default new Action({
12
12
  method: 'POST',
13
13
 
14
14
  async handle(request: RequestInstance) {
15
- const id = Number(request.getParam('id'))
16
- const campaign = await Campaign.find(id)
17
- if (!campaign)
15
+ const id = marketingRecordId(request)
16
+ if (!id)
17
+ return response.json({ message: 'A valid campaign id is required.' }, 400)
18
+
19
+ let context
20
+ try {
21
+ context = await loadCampaignDeliveryContext(id)
22
+ }
23
+ catch (error) {
24
+ return dashboardOperationalError(error, 'Campaign delivery prerequisites could not be loaded.', 'CampaignSendAction.prerequisites')
25
+ }
26
+ if (!context.campaign)
18
27
  return response.json({ message: 'Campaign not found.' }, 404)
19
28
 
20
- const listId = Number(campaign.get('email_list_id') || 0)
21
- const list = listId ? await EmailList.find(listId) : null
22
- const memberRow = listId
23
- ? await db
24
- .selectFrom('email_list_subscribers')
25
- .select(db.fn.count('id').as('count'))
26
- .where('email_list_id', '=', listId)
27
- .where('status', '=', 'subscribed')
28
- .executeTakeFirst()
29
- : null
30
29
  const validationError = validateCampaignDelivery(
31
- campaign,
30
+ context.campaign,
32
31
  'send',
33
- Number(memberRow?.count || 0),
34
- String(list?.get('status') || ''),
32
+ context.activeMembers,
33
+ context.listStatus,
35
34
  )
36
35
  if (validationError)
37
36
  return response.json({ message: validationError }, 422)
38
37
 
39
- await campaigns.sendNow(id)
40
- return response.json({ id, status: 'sending' }, 202)
38
+ try {
39
+ await campaigns.sendNow(id)
40
+ return response.json({ id, status: 'sending' }, 202)
41
+ }
42
+ catch (error) {
43
+ if (error instanceof CampaignStateConflictError)
44
+ return response.json({ message: 'Campaign delivery state changed. Refresh and try again.' }, 409)
45
+ return dashboardOperationalError(error, 'Campaign could not be queued.', 'CampaignSendAction.queue', 500)
46
+ }
41
47
  },
42
48
  })
@@ -4,6 +4,7 @@ import { config } from '@stacksjs/config'
4
4
  import { Campaign } from '@stacksjs/orm'
5
5
  import { response } from '@stacksjs/router'
6
6
  import { campaignWriteData, validateCampaignWriteData } from './campaign-records'
7
+ import { marketingModelError } from './marketing-response'
7
8
 
8
9
  export default new Action({
9
10
  name: 'CampaignStoreAction',
@@ -12,7 +13,6 @@ export default new Action({
12
13
  model: Campaign,
13
14
 
14
15
  async handle(request: RequestInstance) {
15
- await request.validate()
16
16
  const data = campaignWriteData(
17
17
  await request.all(),
18
18
  String((config as any).commerce?.currency || 'USD').toUpperCase(),
@@ -23,14 +23,19 @@ export default new Action({
23
23
  if (['sending', 'sent'].includes(data.status))
24
24
  return response.json({ message: 'Campaign delivery status is managed by the send pipeline.' }, 422)
25
25
 
26
- const campaign = await Campaign.create({
27
- ...data,
28
- audience_size: 0,
29
- sent_count: 0,
30
- open_rate: null,
31
- click_rate: null,
32
- conversion_rate: null,
33
- })
34
- return response.json({ id: campaign.get('id') }, 201)
26
+ try {
27
+ const campaign = await Campaign.create({
28
+ ...data,
29
+ audience_size: 0,
30
+ sent_count: 0,
31
+ open_rate: null,
32
+ click_rate: null,
33
+ conversion_rate: null,
34
+ })
35
+ return response.json({ id: campaign.get('id') }, 201)
36
+ }
37
+ catch (error) {
38
+ return marketingModelError(error, 'Campaign could not be created.', 'CampaignStoreAction')
39
+ }
35
40
  },
36
41
  })
@@ -4,6 +4,7 @@ import { config } from '@stacksjs/config'
4
4
  import { Campaign } from '@stacksjs/orm'
5
5
  import { response } from '@stacksjs/router'
6
6
  import { campaignWriteData, validateCampaignWriteData } from './campaign-records'
7
+ import { marketingModelError, marketingRecordId } from './marketing-response'
7
8
 
8
9
  export default new Action({
9
10
  name: 'CampaignUpdateAction',
@@ -12,16 +13,9 @@ export default new Action({
12
13
  model: Campaign,
13
14
 
14
15
  async handle(request: RequestInstance) {
15
- await request.validate()
16
- const id = Number(request.getParam('id'))
17
- const campaign = await Campaign.find(id)
18
- if (!campaign)
19
- return response.json({ message: 'Campaign not found.' }, 404)
20
-
21
- const currentStatus = String(campaign.get('status') || '')
22
- if (['sending', 'sent'].includes(currentStatus))
23
- return response.json({ message: 'Campaigns that entered delivery cannot be edited.' }, 409)
24
-
16
+ const id = marketingRecordId(request)
17
+ if (!id)
18
+ return response.json({ message: 'A valid campaign id is required.' }, 400)
25
19
  const data = campaignWriteData(
26
20
  await request.all(),
27
21
  String((config as any).commerce?.currency || 'USD').toUpperCase(),
@@ -32,7 +26,20 @@ export default new Action({
32
26
  if (['sending', 'sent'].includes(data.status))
33
27
  return response.json({ message: 'Campaign delivery status is managed by the send pipeline.' }, 422)
34
28
 
35
- await campaign.update(data)
36
- return response.json({ id })
29
+ try {
30
+ const campaign = await Campaign.find(id)
31
+ if (!campaign)
32
+ return response.json({ message: 'Campaign not found.' }, 404)
33
+
34
+ const currentStatus = String(campaign.get('status') || '')
35
+ if (['sending', 'sent'].includes(currentStatus))
36
+ return response.json({ message: 'Campaigns that entered delivery cannot be edited.' }, 409)
37
+
38
+ await campaign.update(data)
39
+ return response.json({ id })
40
+ }
41
+ catch (error) {
42
+ return marketingModelError(error, 'Campaign could not be updated.', 'CampaignUpdateAction')
43
+ }
37
44
  },
38
45
  })
@@ -3,6 +3,7 @@ import { Action } from '@stacksjs/actions'
3
3
  import { db } from '@stacksjs/database'
4
4
  import { EmailList } from '@stacksjs/orm'
5
5
  import { response } from '@stacksjs/router'
6
+ import { marketingModelError, marketingRecordId } from './marketing-response'
6
7
 
7
8
  export default new Action({
8
9
  name: 'MarketingListDestroyAction',
@@ -10,32 +11,40 @@ export default new Action({
10
11
  method: 'DELETE',
11
12
 
12
13
  async handle(request: RequestInstance) {
13
- const id = Number(request.getParam('id'))
14
- const list = await EmailList.find(id)
15
- if (!list)
16
- return response.json({ message: 'Email list not found.' }, 404)
14
+ const id = marketingRecordId(request)
15
+ if (!id)
16
+ return response.json({ message: 'A valid email list id is required.' }, 400)
17
17
 
18
- const [membership, campaign] = await Promise.all([
19
- db
20
- .selectFrom('email_list_subscribers')
21
- .select(db.fn.count('id').as('count'))
22
- .where('email_list_id', '=', id)
23
- .executeTakeFirst(),
24
- db
25
- .selectFrom('campaigns')
26
- .select(db.fn.count('id').as('count'))
27
- .where('email_list_id', '=', id)
28
- .executeTakeFirst(),
29
- ])
30
- const membershipCount = Number(membership?.count || 0)
31
- const campaignCount = Number(campaign?.count || 0)
32
- if (membershipCount > 0 || campaignCount > 0) {
33
- return response.json({
34
- message: 'Archive this list instead. Lists with memberships or campaigns cannot be deleted.',
35
- }, 409)
36
- }
18
+ try {
19
+ const list = await EmailList.find(id)
20
+ if (!list)
21
+ return response.json({ message: 'Email list not found.' }, 404)
22
+
23
+ const [membership, campaign] = await Promise.all([
24
+ db
25
+ .selectFrom('email_list_subscribers')
26
+ .select(db.fn.count('id').as('count'))
27
+ .where('email_list_id', '=', id)
28
+ .executeTakeFirst(),
29
+ db
30
+ .selectFrom('campaigns')
31
+ .select(db.fn.count('id').as('count'))
32
+ .where('email_list_id', '=', id)
33
+ .executeTakeFirst(),
34
+ ])
35
+ const membershipCount = Number(membership?.count || 0)
36
+ const campaignCount = Number(campaign?.count || 0)
37
+ if (membershipCount > 0 || campaignCount > 0) {
38
+ return response.json({
39
+ message: 'Archive this list instead. Lists with memberships or campaigns cannot be deleted.',
40
+ }, 409)
41
+ }
37
42
 
38
- await list.delete()
39
- return response.noContent()
43
+ await list.delete()
44
+ return response.noContent()
45
+ }
46
+ catch (error) {
47
+ return marketingModelError(error, 'Email list could not be deleted.', 'ListDestroyAction')
48
+ }
40
49
  },
41
50
  })
@@ -1,6 +1,7 @@
1
1
  import { Action } from '@stacksjs/actions'
2
2
  import { db } from '@stacksjs/database'
3
3
  import { EmailList } from '@stacksjs/orm'
4
+ import { dashboardOperationalError } from '../dashboard-response'
4
5
  import { normalizeMarketingLists } from './marketing-list-records'
5
6
 
6
7
  export default new Action({
@@ -15,32 +16,37 @@ export default new Action({
15
16
  .slice(0, 19)
16
17
  .replace('T', ' ')
17
18
 
18
- const [lists, membershipRows, newMembershipRows, campaignRows] = await Promise.all([
19
- EmailList.orderBy('name', 'asc').get(),
20
- db
21
- .selectFrom('email_list_subscribers')
22
- .select(['email_list_id', 'status', db.fn.count('id').as('count')])
23
- .groupBy(['email_list_id', 'status'])
24
- .execute(),
25
- db
26
- .selectFrom('email_list_subscribers')
27
- .select(['email_list_id', db.fn.count('id').as('count')])
28
- .where('status', '=', 'subscribed')
29
- .where('subscribed_at', '>=', weekStart)
30
- .groupBy('email_list_id')
31
- .execute(),
32
- db
33
- .selectFrom('campaigns')
34
- .select([
35
- 'email_list_id',
36
- db.fn.count('id').as('count'),
37
- db.fn.max('sent_at').as('last_sent_at'),
38
- ])
39
- .whereNotNull('email_list_id')
40
- .groupBy('email_list_id')
41
- .execute(),
42
- ])
19
+ try {
20
+ const [lists, membershipRows, newMembershipRows, campaignRows] = await Promise.all([
21
+ EmailList.orderBy('name', 'asc').get(),
22
+ db
23
+ .selectFrom('email_list_subscribers')
24
+ .select(['email_list_id', 'status', db.fn.count('id').as('count')])
25
+ .groupBy(['email_list_id', 'status'])
26
+ .execute(),
27
+ db
28
+ .selectFrom('email_list_subscribers')
29
+ .select(['email_list_id', db.fn.count('id').as('count')])
30
+ .where('status', '=', 'subscribed')
31
+ .where('subscribed_at', '>=', weekStart)
32
+ .groupBy('email_list_id')
33
+ .execute(),
34
+ db
35
+ .selectFrom('campaigns')
36
+ .select([
37
+ 'email_list_id',
38
+ db.fn.count('id').as('count'),
39
+ db.fn.max('sent_at').as('last_sent_at'),
40
+ ])
41
+ .whereNotNull('email_list_id')
42
+ .groupBy('email_list_id')
43
+ .execute(),
44
+ ])
43
45
 
44
- return normalizeMarketingLists(lists, membershipRows, newMembershipRows, campaignRows)
46
+ return normalizeMarketingLists(lists, membershipRows, newMembershipRows, campaignRows)
47
+ }
48
+ catch (error) {
49
+ return dashboardOperationalError(error, 'Email lists could not be loaded.', 'ListIndexAction')
50
+ }
45
51
  },
46
52
  })