@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
@@ -2,7 +2,8 @@ import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { EmailList } from '@stacksjs/orm'
4
4
  import { response } from '@stacksjs/router'
5
- import { marketingListWriteData } from './marketing-list-records'
5
+ import { marketingListWriteData, validateMarketingListWriteData } from './marketing-list-records'
6
+ import { marketingModelError } from './marketing-response'
6
7
 
7
8
  export default new Action({
8
9
  name: 'MarketingListStoreAction',
@@ -11,26 +12,38 @@ export default new Action({
11
12
  model: EmailList,
12
13
 
13
14
  async handle(request: RequestInstance) {
14
- await request.validate()
15
15
  const data = marketingListWriteData(await request.all())
16
+ const validationError = validateMarketingListWriteData(data)
17
+ if (validationError)
18
+ return response.json({ message: validationError }, 422)
16
19
 
17
- const duplicate = await EmailList.where('slug', data.slug).first()
18
- if (duplicate)
19
- return response.json({ message: 'An email list with this slug already exists.' }, 422)
20
+ try {
21
+ const duplicate = await EmailList.where('slug', data.slug).first()
22
+ if (duplicate)
23
+ return response.json({ message: 'An email list with this slug already exists.' }, 422)
20
24
 
21
- const list = await EmailList.create({
22
- name: data.name,
23
- slug: data.slug,
24
- description: data.description,
25
- status: data.status,
26
- is_public: data.isPublic,
27
- double_opt_in: data.doubleOptIn,
28
- subscriber_count: 0,
29
- active_count: 0,
30
- unsubscribed_count: 0,
31
- bounced_count: 0,
32
- })
25
+ const list = await EmailList.create({
26
+ name: data.name,
27
+ slug: data.slug,
28
+ description: data.description,
29
+ status: data.status,
30
+ is_public: data.isPublic,
31
+ double_opt_in: data.doubleOptIn,
32
+ subscriber_count: 0,
33
+ active_count: 0,
34
+ unsubscribed_count: 0,
35
+ bounced_count: 0,
36
+ })
33
37
 
34
- return response.json({ id: list.get('id') }, 201)
38
+ return response.json({ id: list.get('id') }, 201)
39
+ }
40
+ catch (error) {
41
+ return marketingModelError(
42
+ error,
43
+ 'Email list could not be created.',
44
+ 'ListStoreAction',
45
+ 'An email list with this slug already exists.',
46
+ )
47
+ }
35
48
  },
36
49
  })
@@ -2,7 +2,8 @@ import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { EmailList } from '@stacksjs/orm'
4
4
  import { response } from '@stacksjs/router'
5
- import { marketingListWriteData } from './marketing-list-records'
5
+ import { marketingListWriteData, validateMarketingListWriteData } from './marketing-list-records'
6
+ import { marketingModelError, marketingRecordId } from './marketing-response'
6
7
 
7
8
  export default new Action({
8
9
  name: 'MarketingListUpdateAction',
@@ -11,25 +12,40 @@ export default new Action({
11
12
  model: EmailList,
12
13
 
13
14
  async handle(request: RequestInstance) {
14
- await request.validate()
15
- const id = Number(request.getParam('id'))
16
- const list = await EmailList.find(id)
17
- if (!list)
18
- return response.json({ message: 'Email list not found.' }, 404)
19
-
15
+ const id = marketingRecordId(request)
16
+ if (!id)
17
+ return response.json({ message: 'A valid email list id is required.' }, 400)
20
18
  const data = marketingListWriteData(await request.all())
21
- const duplicates = await EmailList.where('slug', data.slug).get()
22
- if (duplicates.some(record => Number(record.get('id')) !== id))
23
- return response.json({ message: 'An email list with this slug already exists.' }, 422)
19
+ const validationError = validateMarketingListWriteData(data)
20
+ if (validationError)
21
+ return response.json({ message: validationError }, 422)
22
+
23
+ try {
24
+ const list = await EmailList.find(id)
25
+ if (!list)
26
+ return response.json({ message: 'Email list not found.' }, 404)
27
+
28
+ const duplicates = await EmailList.where('slug', data.slug).get()
29
+ if (duplicates.some(record => Number(record.get('id')) !== id))
30
+ return response.json({ message: 'An email list with this slug already exists.' }, 422)
24
31
 
25
- await list.update({
26
- name: data.name,
27
- slug: data.slug,
28
- description: data.description,
29
- status: data.status,
30
- is_public: data.isPublic,
31
- double_opt_in: data.doubleOptIn,
32
- })
33
- return response.json({ id })
32
+ await list.update({
33
+ name: data.name,
34
+ slug: data.slug,
35
+ description: data.description,
36
+ status: data.status,
37
+ is_public: data.isPublic,
38
+ double_opt_in: data.doubleOptIn,
39
+ })
40
+ return response.json({ id })
41
+ }
42
+ catch (error) {
43
+ return marketingModelError(
44
+ error,
45
+ 'Email list could not be updated.',
46
+ 'ListUpdateAction',
47
+ 'An email list with this slug already exists.',
48
+ )
49
+ }
34
50
  },
35
51
  })
@@ -2,6 +2,7 @@ import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { SocialPost } from '@stacksjs/orm'
4
4
  import { response } from '@stacksjs/router'
5
+ import { marketingModelError, marketingRecordId } from './marketing-response'
5
6
 
6
7
  export default new Action({
7
8
  name: 'SocialPostDestroyAction',
@@ -9,10 +10,19 @@ export default new Action({
9
10
  method: 'DELETE',
10
11
 
11
12
  async handle(request: RequestInstance) {
12
- const post = await SocialPost.find(Number(request.getParam('id')))
13
- if (!post)
14
- return response.json({ message: 'Social post not found.' }, 404)
15
- await post.delete()
16
- return response.noContent()
13
+ const id = marketingRecordId(request)
14
+ if (!id)
15
+ return response.json({ message: 'A valid social post id is required.' }, 400)
16
+
17
+ try {
18
+ const post = await SocialPost.find(id)
19
+ if (!post)
20
+ return response.json({ message: 'Social post not found.' }, 404)
21
+ await post.delete()
22
+ return response.noContent()
23
+ }
24
+ catch (error) {
25
+ return marketingModelError(error, 'Social post could not be deleted.', 'SocialPostDestroyAction')
26
+ }
17
27
  },
18
28
  })
@@ -1,5 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
2
  import { SocialPost, User } from '@stacksjs/orm'
3
+ import { dashboardOperationalError } from '../dashboard-response'
3
4
  import { normalizeSocialPosts } from './social-post-records'
4
5
 
5
6
  export default new Action({
@@ -9,10 +10,15 @@ export default new Action({
9
10
  apiResponse: true,
10
11
 
11
12
  async handle() {
12
- const [posts, users] = await Promise.all([
13
- SocialPost.orderByDesc('id').limit(500).get(),
14
- User.orderBy('name', 'asc').limit(500).get(),
15
- ])
16
- return normalizeSocialPosts(posts, users)
13
+ try {
14
+ const [posts, users] = await Promise.all([
15
+ SocialPost.orderByDesc('id').limit(500).get(),
16
+ User.orderBy('name', 'asc').limit(500).get(),
17
+ ])
18
+ return normalizeSocialPosts(posts, users)
19
+ }
20
+ catch (error) {
21
+ return dashboardOperationalError(error, 'Social posts could not be loaded.', 'SocialPostIndexAction')
22
+ }
17
23
  },
18
24
  })
@@ -2,7 +2,8 @@ import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { SocialPost } from '@stacksjs/orm'
4
4
  import { response } from '@stacksjs/router'
5
- import { socialPostWriteData, validateSocialPostSchedule } from './social-post-records'
5
+ import { marketingModelError } from './marketing-response'
6
+ import { socialPostWriteData, validateSocialPostWriteData } from './social-post-records'
6
7
 
7
8
  export default new Action({
8
9
  name: 'SocialPostStoreAction',
@@ -11,19 +12,24 @@ export default new Action({
11
12
  model: SocialPost,
12
13
 
13
14
  async handle(request: RequestInstance) {
14
- await request.validate()
15
15
  const data = socialPostWriteData(await request.all())
16
- const scheduleError = validateSocialPostSchedule(data)
17
- if (scheduleError)
18
- return response.json({ message: scheduleError }, 422)
16
+ const validationError = validateSocialPostWriteData(data)
17
+ if (validationError)
18
+ return response.json({ message: validationError }, 422)
19
19
 
20
- const post = await SocialPost.create({
21
- ...data,
22
- likes: 0,
23
- shares: 0,
24
- comments: 0,
25
- reach: 0,
26
- })
27
- return response.json({ id: post.get('id') }, 201)
20
+ try {
21
+ const post = await SocialPost.create({
22
+ ...data,
23
+ platform: data.platform as Exclude<typeof data.platform, ''>,
24
+ likes: 0,
25
+ shares: 0,
26
+ comments: 0,
27
+ reach: 0,
28
+ })
29
+ return response.json({ id: post.get('id') }, 201)
30
+ }
31
+ catch (error) {
32
+ return marketingModelError(error, 'Social post could not be created.', 'SocialPostStoreAction')
33
+ }
28
34
  },
29
35
  })
@@ -2,7 +2,8 @@ import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { SocialPost } from '@stacksjs/orm'
4
4
  import { response } from '@stacksjs/router'
5
- import { socialPostWriteData, validateSocialPostSchedule } from './social-post-records'
5
+ import { marketingModelError, marketingRecordId } from './marketing-response'
6
+ import { socialPostWriteData, validateSocialPostWriteData } from './social-post-records'
6
7
 
7
8
  export default new Action({
8
9
  name: 'SocialPostUpdateAction',
@@ -11,18 +12,27 @@ export default new Action({
11
12
  model: SocialPost,
12
13
 
13
14
  async handle(request: RequestInstance) {
14
- await request.validate()
15
- const id = Number(request.getParam('id'))
16
- const post = await SocialPost.find(id)
17
- if (!post)
18
- return response.json({ message: 'Social post not found.' }, 404)
19
-
15
+ const id = marketingRecordId(request)
16
+ if (!id)
17
+ return response.json({ message: 'A valid social post id is required.' }, 400)
20
18
  const data = socialPostWriteData(await request.all())
21
- const scheduleError = validateSocialPostSchedule(data)
22
- if (scheduleError)
23
- return response.json({ message: scheduleError }, 422)
19
+ const validationError = validateSocialPostWriteData(data)
20
+ if (validationError)
21
+ return response.json({ message: validationError }, 422)
22
+
23
+ try {
24
+ const post = await SocialPost.find(id)
25
+ if (!post)
26
+ return response.json({ message: 'Social post not found.' }, 404)
24
27
 
25
- await post.update(data)
26
- return response.json({ id })
28
+ await post.update({
29
+ ...data,
30
+ platform: data.platform as Exclude<typeof data.platform, ''>,
31
+ })
32
+ return response.json({ id })
33
+ }
34
+ catch (error) {
35
+ return marketingModelError(error, 'Social post could not be updated.', 'SocialPostUpdateAction')
36
+ }
27
37
  },
28
38
  })
@@ -1,5 +1,39 @@
1
+ import { db } from '@stacksjs/database'
2
+ import { Campaign, EmailList } from '@stacksjs/orm'
3
+
1
4
  export type CampaignDeliveryOperation = 'send' | 'schedule' | 'cancel'
2
5
 
6
+ export interface CampaignDeliveryContext {
7
+ campaign: any | null
8
+ activeMembers: number
9
+ listStatus: string
10
+ }
11
+
12
+ export async function loadCampaignDeliveryContext(id: number): Promise<CampaignDeliveryContext> {
13
+ const campaign = await Campaign.find(id)
14
+ if (!campaign)
15
+ return { campaign: null, activeMembers: 0, listStatus: '' }
16
+
17
+ const listId = Number(campaign.get('email_list_id') || 0)
18
+ const [list, memberRow] = listId
19
+ ? await Promise.all([
20
+ EmailList.find(listId),
21
+ db
22
+ .selectFrom('email_list_subscribers')
23
+ .select(db.fn.count('id').as('count'))
24
+ .where('email_list_id', '=', listId)
25
+ .where('status', '=', 'subscribed')
26
+ .executeTakeFirst(),
27
+ ])
28
+ : [null, null]
29
+
30
+ return {
31
+ campaign,
32
+ activeMembers: Number(memberRow?.count || 0),
33
+ listStatus: String(list?.get('status') || ''),
34
+ }
35
+ }
36
+
3
37
  function value(record: any, ...keys: string[]): unknown {
4
38
  for (const key of keys) {
5
39
  const result = typeof record?.get === 'function' ? record.get(key) : record?.[key]
@@ -3,6 +3,7 @@ import {
3
3
  marketingListWriteData,
4
4
  normalizeMarketingLists,
5
5
  slugifyMarketingList,
6
+ validateMarketingListWriteData,
6
7
  } from './marketing-list-records'
7
8
 
8
9
  describe('marketing list records', () => {
@@ -68,4 +69,13 @@ describe('marketing list records', () => {
68
69
  doubleOptIn: 0,
69
70
  })
70
71
  })
72
+
73
+ test('rejects empty and oversized dashboard writes before model defaults apply', () => {
74
+ expect(validateMarketingListWriteData(marketingListWriteData({})))
75
+ .toBe('List name must be between 2 and 100 characters.')
76
+ expect(validateMarketingListWriteData(marketingListWriteData({
77
+ name: 'Product News',
78
+ description: 'x'.repeat(501),
79
+ }))).toBe('List description must be 500 characters or fewer.')
80
+ })
71
81
  })
@@ -159,3 +159,13 @@ export function marketingListWriteData(input: Record<string, unknown>): {
159
159
  doubleOptIn: boolean(input.doubleOptIn ?? input.double_opt_in) ? 1 : 0,
160
160
  }
161
161
  }
162
+
163
+ export function validateMarketingListWriteData(data: ReturnType<typeof marketingListWriteData>): string {
164
+ if (data.name.length < 2 || data.name.length > 100)
165
+ return 'List name must be between 2 and 100 characters.'
166
+ if (!data.slug || data.slug.length > 120)
167
+ return 'List slug must be between 1 and 120 characters.'
168
+ if (data.description.length > 500)
169
+ return 'List description must be 500 characters or fewer.'
170
+ return ''
171
+ }
@@ -0,0 +1,28 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
2
+ import { isUniqueViolation, ModelValidationError } from '@stacksjs/orm'
3
+ import { response } from '@stacksjs/router'
4
+ import { dashboardOperationalError } from '../dashboard-response'
5
+
6
+ export function marketingRecordId(request: RequestInstance): number | null {
7
+ const id = Number(request.getParam('id'))
8
+ return Number.isSafeInteger(id) && id > 0 ? id : null
9
+ }
10
+
11
+ export function marketingModelError(
12
+ error: unknown,
13
+ message: string,
14
+ action: string,
15
+ duplicateMessage?: string,
16
+ ): Response {
17
+ if (error instanceof ModelValidationError) {
18
+ return response.json({
19
+ message: 'Validation failed.',
20
+ errors: error.errors,
21
+ }, 422)
22
+ }
23
+
24
+ if (duplicateMessage && isUniqueViolation(error))
25
+ return response.json({ message: duplicateMessage }, 422)
26
+
27
+ return dashboardOperationalError(error, message, action, 500)
28
+ }
@@ -3,6 +3,7 @@ import {
3
3
  normalizeSocialPosts,
4
4
  socialPostWriteData,
5
5
  validateSocialPostSchedule,
6
+ validateSocialPostWriteData,
6
7
  } from './social-post-records'
7
8
 
8
9
  const now = new Date('2026-07-29T12:00:00.000Z')
@@ -68,4 +69,16 @@ describe('social post records', () => {
68
69
  }, now)
69
70
  expect(validateSocialPostSchedule(stale, now)).toBe('Schedule time must be in the future.')
70
71
  })
72
+
73
+ test('rejects missing content, invalid platforms, and unsafe image URLs', () => {
74
+ expect(validateSocialPostWriteData(socialPostWriteData({})))
75
+ .toBe('Post content must be between 1 and 2000 characters.')
76
+ expect(validateSocialPostWriteData(socialPostWriteData({ content: 'Ready to publish' })))
77
+ .toBe('Choose a valid social platform.')
78
+ expect(validateSocialPostWriteData(socialPostWriteData({
79
+ content: 'Ready to publish',
80
+ platform: 'linkedin',
81
+ imageUrl: 'javascript:alert(1)',
82
+ }))).toBe('Image URL must use HTTP or HTTPS.')
83
+ })
71
84
  })
@@ -55,11 +55,11 @@ function number(input: unknown): number {
55
55
  return Number.isFinite(result) && result >= 0 ? result : 0
56
56
  }
57
57
 
58
- function platform(input: unknown): SocialPostPlatform {
58
+ function platform(input: unknown): SocialPostPlatform | '' {
59
59
  const normalized = text(input).toLowerCase()
60
60
  return ['twitter', 'facebook', 'instagram', 'linkedin', 'tiktok', 'youtube'].includes(normalized)
61
61
  ? normalized as SocialPostPlatform
62
- : 'twitter'
62
+ : ''
63
63
  }
64
64
 
65
65
  function status(input: unknown): SocialPostStatus {
@@ -89,7 +89,7 @@ export function normalizeSocialPosts(
89
89
  return {
90
90
  id: text(value(post, 'id')),
91
91
  content: text(value(post, 'content')),
92
- platform: platform(value(post, 'platform')),
92
+ platform: platform(value(post, 'platform')) || 'twitter',
93
93
  status: status(value(post, 'status')),
94
94
  scheduledAt: text(value(post, 'scheduled_at', 'scheduledAt')),
95
95
  publishedAt: text(value(post, 'published_at', 'publishedAt')),
@@ -124,7 +124,7 @@ export function normalizeSocialPosts(
124
124
 
125
125
  export function socialPostWriteData(input: Record<string, unknown>, now = new Date()): {
126
126
  content: string
127
- platform: SocialPostPlatform
127
+ platform: SocialPostPlatform | ''
128
128
  status: SocialPostStatus
129
129
  scheduled_at: string | null
130
130
  published_at: string | null
@@ -161,3 +161,21 @@ export function validateSocialPostSchedule(data: ReturnType<typeof socialPostWri
161
161
  return 'Schedule time must be in the future.'
162
162
  return ''
163
163
  }
164
+
165
+ export function validateSocialPostWriteData(data: ReturnType<typeof socialPostWriteData>): string {
166
+ if (data.content.length < 1 || data.content.length > 2000)
167
+ return 'Post content must be between 1 and 2000 characters.'
168
+ if (!data.platform)
169
+ return 'Choose a valid social platform.'
170
+ if (data.image_url) {
171
+ try {
172
+ const url = new URL(data.image_url)
173
+ if (!['http:', 'https:'].includes(url.protocol))
174
+ return 'Image URL must use HTTP or HTTPS.'
175
+ }
176
+ catch {
177
+ return 'Image URL must be a valid URL.'
178
+ }
179
+ }
180
+ return validateSocialPostSchedule(data)
181
+ }
@@ -3,15 +3,18 @@ import { Glob } from 'bun'
3
3
  import { Action } from '@stacksjs/actions'
4
4
  import { path } from '@stacksjs/path'
5
5
  import { countRows } from '../../../../resources/functions/dashboard/data'
6
+ import { dashboardOperationalError } from '../dashboard-response'
6
7
 
7
8
  export default new Action({
8
9
  name: 'GetModels',
9
10
  description: 'Gets the application models.',
11
+ method: 'GET',
10
12
 
11
13
  async handle() {
12
- const glob = new Glob('**/*.ts')
13
- const scanOptions = { cwd: path.userModelsPath(), onlyFiles: true }
14
- const displayModels: Model[] = []
14
+ try {
15
+ const glob = new Glob('**/*.ts')
16
+ const scanOptions = { cwd: path.userModelsPath(), onlyFiles: true }
17
+ const displayModels: Model[] = []
15
18
 
16
19
  for await (const file of glob.scan(scanOptions)) {
17
20
  const model = (await import(path.userModelsPath(file))).default as Model
@@ -31,9 +34,13 @@ export default new Action({
31
34
  displayModels.length = 8
32
35
  displayModels.sort((a, b) => (a.name || '').localeCompare(b.name || ''))
33
36
 
34
- return await Promise.all(displayModels.map(async model => ({
35
- model: model.name,
36
- total: await countRows(model),
37
- })))
37
+ return await Promise.all(displayModels.map(async model => ({
38
+ model: model.name,
39
+ total: await countRows(model),
40
+ })))
41
+ }
42
+ catch (error) {
43
+ return dashboardOperationalError(error, 'Dashboard models could not be loaded.', 'GetModels')
44
+ }
38
45
  },
39
46
  })
@@ -1,12 +1,19 @@
1
1
  import { Action } from '@stacksjs/actions'
2
2
  import { Subscriber } from '@stacksjs/orm'
3
+ import { dashboardOperationalError } from '../dashboard-response'
3
4
 
4
5
  export default new Action({
5
6
  name: 'GetSubscriberCount',
6
7
  description: 'Gets the total number of subscribers.',
8
+ method: 'GET',
7
9
  apiResponse: true,
8
10
 
9
11
  async handle() {
10
- return await Subscriber.count()
12
+ try {
13
+ return await Subscriber.count()
14
+ }
15
+ catch (error) {
16
+ return dashboardOperationalError(error, 'Subscriber total could not be loaded.', 'GetSubscriberCount')
17
+ }
11
18
  },
12
19
  })
@@ -1,12 +1,19 @@
1
1
  import { Action } from '@stacksjs/actions'
2
2
  import { User } from '@stacksjs/orm'
3
+ import { dashboardOperationalError } from '../dashboard-response'
3
4
 
4
5
  export default new Action({
5
6
  name: 'GetUserCount',
6
7
  description: 'Gets the total number of users.',
8
+ method: 'GET',
7
9
  apiResponse: true,
8
10
 
9
11
  async handle() {
10
- return User.count()
12
+ try {
13
+ return await User.count()
14
+ }
15
+ catch (error) {
16
+ return dashboardOperationalError(error, 'User total could not be loaded.', 'GetUserCount')
17
+ }
11
18
  },
12
19
  })
@@ -1,5 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
2
  import { Release } from '@stacksjs/orm'
3
+ import { dashboardOperationalError } from '../dashboard-response'
3
4
  import { normalizeReleaseRecord, summarizeReleases } from './release-records'
4
5
 
5
6
  export default new Action({
@@ -9,12 +10,17 @@ export default new Action({
9
10
  apiResponse: true,
10
11
 
11
12
  async handle() {
12
- const records = await Release.orderByDesc('id').limit(200).get()
13
- const releases = records.map(normalizeReleaseRecord)
13
+ try {
14
+ const records = await Release.orderByDesc('id').limit(200).get()
15
+ const releases = records.map(normalizeReleaseRecord)
14
16
 
15
- return {
16
- releases,
17
- summary: summarizeReleases(releases),
17
+ return {
18
+ releases,
19
+ summary: summarizeReleases(releases),
20
+ }
21
+ }
22
+ catch (error) {
23
+ return dashboardOperationalError(error, 'Release history could not be loaded.', 'ReleaseIndexAction')
18
24
  }
19
25
  },
20
26
  })
@@ -1,4 +1,5 @@
1
1
  import { Action } from '@stacksjs/actions'
2
+ import { dashboardOperationalError } from '../dashboard-response'
2
3
  import { readMailSettings } from './mail-settings'
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
- { settings: await readMailSettings() },
13
- { headers: { 'Cache-Control': 'no-store', Pragma: 'no-cache' } },
14
- )
12
+ try {
13
+ return Response.json(
14
+ { settings: await readMailSettings() },
15
+ { headers: { 'Cache-Control': 'no-store', Pragma: 'no-cache' } },
16
+ )
17
+ }
18
+ catch (error) {
19
+ return dashboardOperationalError(error, 'Mail settings could not be loaded.', 'MailSettingsGetAction')
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 { updateMailSettings } from './mail-settings'
5
6
 
6
7
  export default new Action({
@@ -10,7 +11,13 @@ export default new Action({
10
11
  apiResponse: true,
11
12
 
12
13
  async handle(request: RequestInstance) {
13
- const result = await updateMailSettings(await request.all())
14
+ let result
15
+ try {
16
+ result = await updateMailSettings(await request.all())
17
+ }
18
+ catch (error) {
19
+ return dashboardOperationalError(error, 'Mail settings could not be saved.', 'MailSettingsUpdateAction', 500)
20
+ }
14
21
 
15
22
  if ('validation' in result) {
16
23
  return response.json({
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.70.356",
5
+ "version": "0.70.358",
6
6
  "description": "A modern Stacks development environment.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/defaults",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.356",
5
+ "version": "0.70.358",
6
6
  "description": "The complete managed Stacks application scaffold, including runtime defaults, AI guidance, editor metadata, and npm-backed project support files.",
7
7
  "author": "Chris Breuer",
8
8
  "license": "MIT",