@stacksjs/defaults 0.70.357 → 0.70.362

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 (55) hide show
  1. package/ai/skills/stacks-browse/SKILL.md +10 -5
  2. package/ai/skills/stacks-browse/scripts/browse.ts +109 -13
  3. package/app/Actions/AI/AskAction.ts +12 -17
  4. package/app/Actions/AI/SummaryAction.ts +12 -17
  5. package/app/Actions/Dashboard/Actions/GetActions.ts +15 -9
  6. package/app/Actions/Dashboard/Analytics/EventStoreAction.ts +21 -9
  7. package/app/Actions/Dashboard/Commerce/ProductUnitDefaultAction.ts +12 -3
  8. package/app/Actions/Dashboard/Commerce/TaxRateDefaultAction.ts +12 -3
  9. package/app/Actions/Dashboard/DashboardHealthAction.ts +34 -14
  10. package/app/Actions/Dashboard/DashboardHomeAction.test.ts +1 -1
  11. package/app/Actions/Dashboard/DashboardHomeAction.ts +31 -4
  12. package/app/Actions/Dashboard/DashboardStatsAction.ts +6 -1
  13. package/app/Actions/Dashboard/Email/InboxSendAction.ts +14 -7
  14. package/app/Actions/Dashboard/Infrastructure/CommandIndexAction.ts +33 -27
  15. package/app/Actions/Dashboard/Infrastructure/EnvironmentIndexAction.ts +10 -4
  16. package/app/Actions/Dashboard/Infrastructure/EnvironmentUpdateAction.ts +8 -1
  17. package/app/Actions/Dashboard/Infrastructure/RequestIndexAction.ts +24 -18
  18. package/app/Actions/Dashboard/Infrastructure/ServerIndexAction.ts +15 -9
  19. package/app/Actions/Dashboard/Infrastructure/ServerShowAction.ts +9 -2
  20. package/app/Actions/Dashboard/Marketing/CampaignCancelAction.ts +23 -5
  21. package/app/Actions/Dashboard/Marketing/CampaignDestroyAction.ts +26 -17
  22. package/app/Actions/Dashboard/Marketing/CampaignIndexAction.ts +49 -43
  23. package/app/Actions/Dashboard/Marketing/CampaignScheduleAction.ts +31 -25
  24. package/app/Actions/Dashboard/Marketing/CampaignSendAction.ts +28 -22
  25. package/app/Actions/Dashboard/Marketing/CampaignStoreAction.ts +15 -10
  26. package/app/Actions/Dashboard/Marketing/CampaignUpdateAction.ts +19 -12
  27. package/app/Actions/Dashboard/Marketing/ListDestroyAction.ts +34 -25
  28. package/app/Actions/Dashboard/Marketing/ListIndexAction.ts +32 -26
  29. package/app/Actions/Dashboard/Marketing/ListStoreAction.ts +31 -18
  30. package/app/Actions/Dashboard/Marketing/ListUpdateAction.ts +35 -19
  31. package/app/Actions/Dashboard/Marketing/SocialPostDestroyAction.ts +15 -5
  32. package/app/Actions/Dashboard/Marketing/SocialPostIndexAction.ts +11 -5
  33. package/app/Actions/Dashboard/Marketing/SocialPostStoreAction.ts +19 -13
  34. package/app/Actions/Dashboard/Marketing/SocialPostUpdateAction.ts +22 -12
  35. package/app/Actions/Dashboard/Marketing/campaign-delivery.ts +34 -0
  36. package/app/Actions/Dashboard/Marketing/marketing-list-records.test.ts +10 -0
  37. package/app/Actions/Dashboard/Marketing/marketing-list-records.ts +10 -0
  38. package/app/Actions/Dashboard/Marketing/marketing-response.ts +28 -0
  39. package/app/Actions/Dashboard/Marketing/social-post-records.test.ts +13 -0
  40. package/app/Actions/Dashboard/Marketing/social-post-records.ts +22 -4
  41. package/app/Actions/Dashboard/Models/GetModels.ts +14 -7
  42. package/app/Actions/Dashboard/Models/GetSubscriberCount.ts +8 -1
  43. package/app/Actions/Dashboard/Models/GetUserCount.ts +8 -1
  44. package/app/Actions/Dashboard/Releases/ReleaseIndexAction.ts +11 -5
  45. package/app/Actions/Dashboard/Settings/MailSettingsGetAction.ts +10 -4
  46. package/app/Actions/Dashboard/Settings/MailSettingsUpdateAction.ts +8 -1
  47. package/app/Actions/LogAction.ts +11 -11
  48. package/ide/vscode/package.json +1 -1
  49. package/package.json +1 -1
  50. package/resources/components/Dashboard/Marketing/CampaignDeleteDialog.stx +11 -12
  51. package/resources/components/Dashboard/Marketing/MarketingListDeleteDialog.stx +15 -15
  52. package/resources/components/Dashboard/Marketing/SocialPostDeleteDialog.stx +11 -12
  53. package/resources/components/Dashboard/UI/ConfirmDialog.stx +51 -53
  54. package/resources/components/Dashboard/UI/Modal.stx +83 -29
  55. package/views/dashboard/stores/auth.ts +18 -2
@@ -2,6 +2,7 @@ import { Action } from '@stacksjs/actions'
2
2
  import { Order, Post, Product, Request, User } from '@stacksjs/orm'
3
3
  import { checkApplicationHealth, type ApplicationHealthCheck } from '@stacksjs/router'
4
4
  import { formatRelative, safeGet } from '../../../resources/functions/dashboard/data'
5
+ import { dashboardOperationalIssue } from './dashboard-response'
5
6
 
6
7
  interface HttpRequestSample {
7
8
  duration: number
@@ -28,7 +29,7 @@ export function serializeHealthCheck(name: string, check: ApplicationHealthCheck
28
29
  name: name.charAt(0).toUpperCase() + name.slice(1),
29
30
  status: check.ok ? 'healthy' : 'critical',
30
31
  latency: `${check.ms}ms`,
31
- detail: check.message || '',
32
+ detail: check.ok ? '' : 'Dependency probe failed.',
32
33
  }
33
34
  }
34
35
 
@@ -43,7 +44,11 @@ function issue(source: string, result: PromiseSettledResult<unknown>) {
43
44
  return result.status === 'rejected'
44
45
  ? {
45
46
  source,
46
- message: result.reason instanceof Error ? result.reason.message : 'Query failed.',
47
+ message: dashboardOperationalIssue(
48
+ result.reason,
49
+ `${source} data could not be loaded.`,
50
+ `DashboardHomeAction.${source.toLowerCase().replaceAll(' ', '-')}`,
51
+ ),
47
52
  }
48
53
  : null
49
54
  }
@@ -65,7 +70,7 @@ export default new Action({
65
70
  Request.count(),
66
71
  Request.orderBy('created_at', 'desc').limit(1000).get(),
67
72
  ])
68
- const health = await checkApplicationHealth()
73
+ const healthResult = await Promise.allSettled([checkApplicationHealth()])
69
74
 
70
75
  const [
71
76
  userCount,
@@ -93,7 +98,19 @@ export default new Action({
93
98
  })))
94
99
  : summarizeHttpRequests(0, [])
95
100
 
96
- const services = Object.entries(health.checks).map(([name, check]) => serializeHealthCheck(name, check))
101
+ const health = healthResult[0]
102
+ const services = health.status === 'fulfilled'
103
+ ? Object.entries(health.value.checks).map(([name, check]) => {
104
+ if (!check.ok) {
105
+ dashboardOperationalIssue(
106
+ check.message,
107
+ 'Dependency probe failed.',
108
+ `DashboardHomeAction.health.${name}`,
109
+ )
110
+ }
111
+ return serializeHealthCheck(name, check)
112
+ })
113
+ : []
97
114
 
98
115
  const activities = [
99
116
  ...(recentOrders.status === 'fulfilled' ? recentOrders.value : []).map((order: any) => ({
@@ -127,6 +144,16 @@ export default new Action({
127
144
  const issues = modelResults
128
145
  .map((result, index) => issue(sources[index], result))
129
146
  .filter((entry): entry is { source: string, message: string } => entry !== null)
147
+ if (health.status === 'rejected') {
148
+ issues.push({
149
+ source: 'System health',
150
+ message: dashboardOperationalIssue(
151
+ health.reason,
152
+ 'System health could not be loaded.',
153
+ 'DashboardHomeAction.health',
154
+ ),
155
+ })
156
+ }
130
157
 
131
158
  return { stats, httpMetrics, services, activities, issues }
132
159
  },
@@ -1,5 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
2
  import { Customer, Order, Post, User } from '@stacksjs/orm'
3
+ import { dashboardOperationalIssue } from './dashboard-response'
3
4
 
4
5
  interface DashboardStatDefinition {
5
6
  title: string
@@ -61,7 +62,11 @@ export default new Action({
61
62
  const issues = results.flatMap((result, index) => result.status === 'rejected'
62
63
  ? [{
63
64
  source: definitions[index].title,
64
- message: result.reason instanceof Error ? result.reason.message : 'Model query failed.',
65
+ message: dashboardOperationalIssue(
66
+ result.reason,
67
+ `${definitions[index].title} data could not be loaded.`,
68
+ `DashboardStatsAction.${definitions[index].title.toLowerCase().replaceAll(' ', '-')}`,
69
+ ),
65
70
  }]
66
71
  : [])
67
72
 
@@ -3,6 +3,7 @@ import { Action } from '@stacksjs/actions'
3
3
  import { notify } from '@stacksjs/notifications'
4
4
  import { response } from '@stacksjs/router'
5
5
  import { schema } from '@stacksjs/validation'
6
+ import { dashboardOperationalError } from '../dashboard-response'
6
7
 
7
8
  export default new Action({
8
9
  name: 'InboxSendAction',
@@ -29,15 +30,21 @@ export default new Action({
29
30
  const subject = String(request.get('subject') || '').trim()
30
31
  const body = String(request.get('body') || '').trim()
31
32
 
32
- const [result] = await notify(
33
- { email: to },
34
- { subject, body, data: { source: 'dashboard-inbox' } },
35
- ['email'],
36
- { ignorePreferences: true },
37
- )
33
+ let result
34
+ try {
35
+ [result] = await notify(
36
+ { email: to },
37
+ { subject, body, data: { source: 'dashboard-inbox' } },
38
+ ['email'],
39
+ { ignorePreferences: true },
40
+ )
41
+ }
42
+ catch (error) {
43
+ return dashboardOperationalError(error, 'The email could not be sent.', 'InboxSendAction.provider', 502)
44
+ }
38
45
 
39
46
  if (!result?.success)
40
- return response.json({ message: result?.error?.message || 'The email could not be sent.' }, 502)
47
+ return dashboardOperationalError(result?.error, 'The email could not be sent.', 'InboxSendAction.result', 502)
41
48
 
42
49
  return response.json({ success: true })
43
50
  },
@@ -3,6 +3,7 @@ import { existsSync, readFileSync, statSync } from 'node:fs'
3
3
  import { join, relative } from 'node:path'
4
4
  import process from 'node:process'
5
5
  import { parseCommandSource } from '../Source/source-inventory'
6
+ import { dashboardOperationalError } from '../dashboard-response'
6
7
 
7
8
  interface CommandConfig {
8
9
  file: string
@@ -15,35 +16,40 @@ export default new Action({
15
16
  description: 'Lists commands registered by the application.',
16
17
  method: 'GET',
17
18
  async handle() {
18
- const projectRoot = process.cwd()
19
- const registryPath = join(projectRoot, 'app/Commands.ts')
20
- const registryModule = await import(registryPath)
21
- const registry = (registryModule.default || {}) as Record<string, string | CommandConfig>
22
- const items = Object.entries(registry).flatMap(([signature, value]) => {
23
- const config = typeof value === 'string'
24
- ? { file: value, enabled: true, aliases: [] }
25
- : { enabled: true, aliases: [], ...value }
26
- const file = join(projectRoot, 'app/Commands', `${config.file}.ts`)
27
- if (!existsSync(file))
28
- return []
19
+ try {
20
+ const projectRoot = process.cwd()
21
+ const registryPath = join(projectRoot, 'app/Commands.ts')
22
+ const registryModule = await import(registryPath)
23
+ const registry = (registryModule.default || {}) as Record<string, string | CommandConfig>
24
+ const items = Object.entries(registry).flatMap(([signature, value]) => {
25
+ const config = typeof value === 'string'
26
+ ? { file: value, enabled: true, aliases: [] }
27
+ : { enabled: true, aliases: [], ...value }
28
+ const file = join(projectRoot, 'app/Commands', `${config.file}.ts`)
29
+ if (!existsSync(file))
30
+ return []
29
31
 
30
- return [parseCommandSource(
31
- readFileSync(file, 'utf8'),
32
- relative(projectRoot, file),
33
- signature,
34
- config.aliases,
35
- statSync(file).mtime.toISOString(),
36
- )]
37
- })
32
+ return [parseCommandSource(
33
+ readFileSync(file, 'utf8'),
34
+ relative(projectRoot, file),
35
+ signature,
36
+ config.aliases,
37
+ statSync(file).mtime.toISOString(),
38
+ )]
39
+ })
38
40
 
39
- return {
40
- items,
41
- stats: {
42
- total: items.length,
43
- aliases: items.reduce((sum, item) => sum + (item.aliases?.length || 0), 0),
44
- options: items.reduce((sum, item) => sum + (item.options?.length || 0), 0),
45
- registered: Object.keys(registry).length,
46
- },
41
+ return {
42
+ items,
43
+ stats: {
44
+ total: items.length,
45
+ aliases: items.reduce((sum, item) => sum + (item.aliases?.length || 0), 0),
46
+ options: items.reduce((sum, item) => sum + (item.options?.length || 0), 0),
47
+ registered: Object.keys(registry).length,
48
+ },
49
+ }
50
+ }
51
+ catch (error) {
52
+ return dashboardOperationalError(error, 'Command sources could not be loaded.', 'CommandIndexAction')
47
53
  }
48
54
  },
49
55
  })
@@ -1,4 +1,5 @@
1
1
  import { Action } from '@stacksjs/actions'
2
+ import { dashboardOperationalError } from '../dashboard-response'
2
3
  import { readEnvironmentFile } from './environment-file'
3
4
 
4
5
  export default new Action({
@@ -8,9 +9,14 @@ export default new Action({
8
9
  apiResponse: true,
9
10
 
10
11
  async handle() {
11
- return Response.json(
12
- { environment: await readEnvironmentFile() },
13
- { headers: { 'Cache-Control': 'no-store', Pragma: 'no-cache' } },
14
- )
12
+ try {
13
+ return Response.json(
14
+ { environment: await readEnvironmentFile() },
15
+ { headers: { 'Cache-Control': 'no-store', Pragma: 'no-cache' } },
16
+ )
17
+ }
18
+ catch (error) {
19
+ return dashboardOperationalError(error, 'Environment file could not be loaded.', 'EnvironmentIndexAction')
20
+ }
15
21
  },
16
22
  })
@@ -1,6 +1,7 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { response } from '@stacksjs/router'
4
+ import { dashboardOperationalError } from '../dashboard-response'
4
5
  import { updateEnvironmentFile } from './environment-file'
5
6
 
6
7
  export default new Action({
@@ -18,7 +19,13 @@ export default new Action({
18
19
  if (typeof revision !== 'string' || !/^[a-f0-9]{64}$/.test(revision))
19
20
  return response.json({ message: 'A valid environment revision is required.' }, 422)
20
21
 
21
- const result = await updateEnvironmentFile(content, revision)
22
+ let result
23
+ try {
24
+ result = await updateEnvironmentFile(content, revision)
25
+ }
26
+ catch (error) {
27
+ return dashboardOperationalError(error, 'Environment file could not be saved.', 'EnvironmentUpdateAction', 500)
28
+ }
22
29
  if (result.issues?.length) {
23
30
  const errors = Object.fromEntries(result.issues.map((issue, index) => [
24
31
  issue.line ? String(issue.line) : `file-${index}`,
@@ -1,19 +1,21 @@
1
1
  import { Action } from '@stacksjs/actions'
2
2
  import { Request } from '@stacksjs/orm'
3
+ import { dashboardOperationalError } from '../dashboard-response'
3
4
 
4
5
  export default new Action({
5
6
  name: 'RequestIndexAction',
6
7
  description: 'Returns request history data for the dashboard.',
7
8
  method: 'GET',
8
9
  async handle() {
9
- const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString()
10
- const [allRequests, total, errorCount, averageDurationMs, requestsLastHour] = await Promise.all([
11
- Request.orderByDesc('id').limit(100).get(),
12
- Request.count(),
13
- Request.where('status_code', '>=', 400).count(),
14
- Request.avg('duration_ms'),
15
- Request.where('created_at', '>=', oneHourAgo).count(),
16
- ])
10
+ try {
11
+ const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString()
12
+ const [allRequests, total, errorCount, averageDurationMs, requestsLastHour] = await Promise.all([
13
+ Request.orderByDesc('id').limit(100).get(),
14
+ Request.count(),
15
+ Request.where('status_code', '>=', 400).count(),
16
+ Request.avg('duration_ms'),
17
+ Request.where('created_at', '>=', oneHourAgo).count(),
18
+ ])
17
19
 
18
20
  const requests = allRequests.map(request => ({
19
21
  id: Number(request.get('id') || 0),
@@ -28,16 +30,20 @@ export default new Action({
28
30
  createdAt: String(request.get('created_at') || ''),
29
31
  }))
30
32
 
31
- return {
32
- requests,
33
- stats: {
34
- total,
35
- errorCount,
36
- averageDurationMs: Number(averageDurationMs || 0),
37
- successRate: total > 0 ? ((total - errorCount) / total) * 100 : 0,
38
- requestsLastHour,
39
- requestsPerMinute: requestsLastHour / 60,
40
- },
33
+ return {
34
+ requests,
35
+ stats: {
36
+ total,
37
+ errorCount,
38
+ averageDurationMs: Number(averageDurationMs || 0),
39
+ successRate: total > 0 ? ((total - errorCount) / total) * 100 : 0,
40
+ requestsLastHour,
41
+ requestsPerMinute: requestsLastHour / 60,
42
+ },
43
+ }
44
+ }
45
+ catch (error) {
46
+ return dashboardOperationalError(error, 'Request history could not be loaded.', 'RequestIndexAction')
41
47
  }
42
48
  },
43
49
  })
@@ -1,21 +1,27 @@
1
1
  import { Action } from '@stacksjs/actions'
2
2
  import { tsCloud } from '~/config/cloud'
3
3
  import { getDashboardCloudSnapshot } from '../Cloud/cloud-overview'
4
+ import { dashboardOperationalError } from '../dashboard-response'
4
5
 
5
6
  export default new Action({
6
7
  name: 'ServerIndexAction',
7
8
  description: 'Returns configured servers and persisted deployment state.',
8
9
  method: 'GET',
9
10
  async handle() {
10
- const snapshot = await getDashboardCloudSnapshot(tsCloud)
11
- return {
12
- project: snapshot.project,
13
- environments: snapshot.environments,
14
- servers: snapshot.serverDefinitions,
15
- deployments: snapshot.deployments,
16
- network: snapshot.resources.filter(resource => resource.category === 'network'),
17
- events: snapshot.events,
18
- generatedAt: snapshot.generatedAt,
11
+ try {
12
+ const snapshot = await getDashboardCloudSnapshot(tsCloud)
13
+ return {
14
+ project: snapshot.project,
15
+ environments: snapshot.environments,
16
+ servers: snapshot.serverDefinitions,
17
+ deployments: snapshot.deployments,
18
+ network: snapshot.resources.filter(resource => resource.category === 'network'),
19
+ events: snapshot.events,
20
+ generatedAt: snapshot.generatedAt,
21
+ }
22
+ }
23
+ catch (error) {
24
+ return dashboardOperationalError(error, 'Server state could not be loaded.', 'ServerIndexAction')
19
25
  }
20
26
  },
21
27
  })
@@ -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
  })