@stacksjs/defaults 0.70.357 → 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.
- package/ai/skills/stacks-browse/SKILL.md +5 -1
- package/ai/skills/stacks-browse/scripts/browse.ts +61 -6
- package/app/Actions/Dashboard/Actions/GetActions.ts +15 -9
- package/app/Actions/Dashboard/Analytics/EventStoreAction.ts +21 -9
- package/app/Actions/Dashboard/Commerce/ProductUnitDefaultAction.ts +12 -3
- package/app/Actions/Dashboard/Commerce/TaxRateDefaultAction.ts +12 -3
- package/app/Actions/Dashboard/DashboardHealthAction.ts +34 -14
- package/app/Actions/Dashboard/DashboardHomeAction.test.ts +1 -1
- package/app/Actions/Dashboard/DashboardHomeAction.ts +31 -4
- package/app/Actions/Dashboard/DashboardStatsAction.ts +6 -1
- package/app/Actions/Dashboard/Email/InboxSendAction.ts +14 -7
- package/app/Actions/Dashboard/Infrastructure/CommandIndexAction.ts +33 -27
- package/app/Actions/Dashboard/Infrastructure/EnvironmentIndexAction.ts +10 -4
- package/app/Actions/Dashboard/Infrastructure/EnvironmentUpdateAction.ts +8 -1
- package/app/Actions/Dashboard/Infrastructure/RequestIndexAction.ts +24 -18
- package/app/Actions/Dashboard/Infrastructure/ServerIndexAction.ts +15 -9
- package/app/Actions/Dashboard/Infrastructure/ServerShowAction.ts +9 -2
- package/app/Actions/Dashboard/Marketing/CampaignCancelAction.ts +23 -5
- package/app/Actions/Dashboard/Marketing/CampaignDestroyAction.ts +26 -17
- package/app/Actions/Dashboard/Marketing/CampaignIndexAction.ts +49 -43
- package/app/Actions/Dashboard/Marketing/CampaignScheduleAction.ts +31 -25
- package/app/Actions/Dashboard/Marketing/CampaignSendAction.ts +28 -22
- package/app/Actions/Dashboard/Marketing/CampaignStoreAction.ts +15 -10
- package/app/Actions/Dashboard/Marketing/CampaignUpdateAction.ts +19 -12
- package/app/Actions/Dashboard/Marketing/ListDestroyAction.ts +34 -25
- package/app/Actions/Dashboard/Marketing/ListIndexAction.ts +32 -26
- package/app/Actions/Dashboard/Marketing/ListStoreAction.ts +31 -18
- package/app/Actions/Dashboard/Marketing/ListUpdateAction.ts +35 -19
- package/app/Actions/Dashboard/Marketing/SocialPostDestroyAction.ts +15 -5
- package/app/Actions/Dashboard/Marketing/SocialPostIndexAction.ts +11 -5
- package/app/Actions/Dashboard/Marketing/SocialPostStoreAction.ts +19 -13
- package/app/Actions/Dashboard/Marketing/SocialPostUpdateAction.ts +22 -12
- package/app/Actions/Dashboard/Marketing/campaign-delivery.ts +34 -0
- package/app/Actions/Dashboard/Marketing/marketing-list-records.test.ts +10 -0
- package/app/Actions/Dashboard/Marketing/marketing-list-records.ts +10 -0
- package/app/Actions/Dashboard/Marketing/marketing-response.ts +28 -0
- package/app/Actions/Dashboard/Marketing/social-post-records.test.ts +13 -0
- package/app/Actions/Dashboard/Marketing/social-post-records.ts +22 -4
- package/app/Actions/Dashboard/Models/GetModels.ts +14 -7
- package/app/Actions/Dashboard/Models/GetSubscriberCount.ts +8 -1
- package/app/Actions/Dashboard/Models/GetUserCount.ts +8 -1
- package/app/Actions/Dashboard/Releases/ReleaseIndexAction.ts +11 -5
- package/app/Actions/Dashboard/Settings/MailSettingsGetAction.ts +10 -4
- package/app/Actions/Dashboard/Settings/MailSettingsUpdateAction.ts +8 -1
- package/ide/vscode/package.json +1 -1
- package/package.json +1 -1
- package/resources/components/Dashboard/UI/Modal.stx +60 -10
- package/views/dashboard/stores/auth.ts +18 -2
|
@@ -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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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 =
|
|
15
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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 =
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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 {
|
|
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 =
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
37
|
-
|
|
39
|
+
context.activeMembers,
|
|
40
|
+
context.listStatus,
|
|
38
41
|
)
|
|
39
42
|
if (validationError)
|
|
40
43
|
return response.json({ message: validationError }, 422)
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return response.json({
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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 {
|
|
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 {
|
|
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 =
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
34
|
-
|
|
32
|
+
context.activeMembers,
|
|
33
|
+
context.listStatus,
|
|
35
34
|
)
|
|
36
35
|
if (validationError)
|
|
37
36
|
return response.json({ message: validationError }, 422)
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
})
|