@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
@@ -1,13 +1,16 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
+ import { db } from '@stacksjs/database'
4
+ import { transaction } from '@stacksjs/orm'
3
5
  import { response } from '@stacksjs/router'
6
+ import { dashboardOperationalError } from '../dashboard-response'
4
7
  import { rowId } from './content-input'
5
8
 
6
9
  /**
7
10
  * `DELETE /api/dashboard/categories/{id}` — deletes a CMS category.
8
11
  *
9
- * Detaches related posts through the model relation before removing the
10
- * category so no pivot rows are orphaned.
12
+ * Detaches related posts through the model-declared pivot before removing the
13
+ * category, with both writes committed by the same transaction.
11
14
  */
12
15
  export default new Action({
13
16
  name: 'CategoryDestroyAction',
@@ -19,13 +22,29 @@ export default new Action({
19
22
  if (!id)
20
23
  return response.json({ message: 'A valid category id is required.' }, 422)
21
24
 
22
- const category = await Category.find(id)
23
- if (!category)
24
- return response.json({ message: 'Category not found.' }, 404)
25
+ try {
26
+ const deleted = await transaction(async (rawTrx) => {
27
+ const trx = rawTrx as unknown as typeof db
28
+ const category = await trx.selectFrom('categories').select(['id']).where('id', '=', id).executeTakeFirst()
29
+ if (!category)
30
+ return false
25
31
 
26
- await category.posts().detach()
27
- await category.delete()
32
+ await trx
33
+ .deleteFrom('categorizable_models')
34
+ .where('category_id', '=', id)
35
+ .where('categorizable_type', '=', 'posts')
36
+ .execute()
37
+ await trx.deleteFrom('categories').where('id', '=', id).execute()
38
+ return true
39
+ })
28
40
 
29
- return response.json({ message: 'Category deleted.', id })
41
+ if (!deleted)
42
+ return response.json({ message: 'Category not found.' }, 404)
43
+
44
+ return response.json({ message: 'Category deleted.', id })
45
+ }
46
+ catch (error) {
47
+ return dashboardOperationalError(error, 'Category could not be deleted.', 'CategoryDestroyAction', 500)
48
+ }
30
49
  },
31
50
  })
@@ -1,5 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
2
  import { db } from '@stacksjs/database'
3
+ import { dashboardOperationalError } from '../dashboard-response'
3
4
 
4
5
  interface CategoryRow {
5
6
  id: number
@@ -29,22 +30,27 @@ export default new Action({
29
30
  method: 'GET',
30
31
  apiResponse: true,
31
32
  async handle() {
32
- const rows = await db
33
- .selectFrom('categories')
34
- .selectAll()
35
- .orderBy('created_at', 'desc')
36
- .execute() as unknown as CategoryRow[]
33
+ try {
34
+ const rows = await db
35
+ .selectFrom('categories')
36
+ .selectAll()
37
+ .orderBy('created_at', 'desc')
38
+ .execute() as unknown as CategoryRow[]
37
39
 
38
- const categories = rows.map(row => ({
39
- id: Number(row.id),
40
- name: String(row.name || ''),
41
- slug: String(row.slug || ''),
42
- description: String(row.description || ''),
43
- is_active: row.is_active == null ? true : Boolean(row.is_active),
44
- created_at: row.created_at || null,
45
- updated_at: row.updated_at || null,
46
- }))
40
+ const categories = rows.map(row => ({
41
+ id: Number(row.id),
42
+ name: String(row.name || ''),
43
+ slug: String(row.slug || ''),
44
+ description: String(row.description || ''),
45
+ is_active: row.is_active == null ? true : Boolean(row.is_active),
46
+ created_at: row.created_at || null,
47
+ updated_at: row.updated_at || null,
48
+ }))
47
49
 
48
- return { categories }
50
+ return { categories }
51
+ }
52
+ catch (error) {
53
+ return dashboardOperationalError(error, 'Categories could not be loaded.', 'CategoryIndexAction')
54
+ }
49
55
  },
50
56
  })
@@ -1,8 +1,10 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { db } from '@stacksjs/database'
4
+ import { transaction } from '@stacksjs/orm'
4
5
  import { response } from '@stacksjs/router'
5
6
  import { randomUUIDv7 } from 'bun'
7
+ import { dashboardOperationalError } from '../dashboard-response'
6
8
  import { findRow, insertedId, slugify, str, timestamp } from './content-input'
7
9
 
8
10
  /**
@@ -26,40 +28,56 @@ export default new Action({
26
28
  if (!slug)
27
29
  return response.json({ message: 'Slug could not be derived from the name; enter one.' }, 422)
28
30
 
29
- const duplicate = await db
30
- .selectFrom('categories')
31
- .select(['id'])
32
- .where('slug', '=', slug)
33
- .executeTakeFirst()
31
+ try {
32
+ const category = await transaction(async (rawTrx) => {
33
+ const trx = rawTrx as unknown as typeof db
34
+ const duplicate = await trx
35
+ .selectFrom('categories')
36
+ .select(['id'])
37
+ .where('slug', '=', slug)
38
+ .executeTakeFirst()
34
39
 
35
- if (duplicate)
36
- return response.json({ message: 'A category with that slug already exists.' }, 422)
40
+ if (duplicate)
41
+ return null
37
42
 
38
- const now = timestamp()
39
- const displayOrder = await db
40
- .selectFrom('categories')
41
- .select(db.fn.max('display_order').as('max_display_order'))
42
- .executeTakeFirst()
43
+ const now = timestamp()
44
+ const displayOrder = await trx
45
+ .selectFrom('categories')
46
+ .select(db.fn.max('display_order').as('max_display_order'))
47
+ .executeTakeFirst()
43
48
 
44
- const result = await db
45
- .insertInto('categories')
46
- .values({
47
- uuid: randomUUIDv7(),
48
- name,
49
- slug,
50
- description,
51
- is_active: 1,
52
- display_order: Number(displayOrder?.max_display_order || 0) + 1,
53
- created_at: now,
54
- updated_at: now,
55
- } as any)
56
- .executeTakeFirst()
49
+ const result = await trx
50
+ .insertInto('categories')
51
+ .values({
52
+ uuid: randomUUIDv7(),
53
+ name,
54
+ slug,
55
+ description,
56
+ is_active: 1,
57
+ display_order: Number(displayOrder?.max_display_order || 0) + 1,
58
+ created_at: now,
59
+ updated_at: now,
60
+ } as any)
61
+ .executeTakeFirst()
57
62
 
58
- const id = insertedId(result)
63
+ const id = insertedId(result)
59
64
 
60
- if (!id)
61
- return response.json({ message: 'Could not create category.' }, 500)
65
+ if (!id)
66
+ throw new Error('Category insert did not return an id.')
62
67
 
63
- return response.json(await findRow('categories', id), 201)
68
+ const created = await findRow('categories', id, trx)
69
+ if (!created)
70
+ throw new Error('Created category could not be loaded.')
71
+ return created
72
+ })
73
+
74
+ if (!category)
75
+ return response.json({ message: 'A category with that slug already exists.' }, 422)
76
+
77
+ return response.json(category, 201)
78
+ }
79
+ catch (error) {
80
+ return dashboardOperationalError(error, 'Category could not be created.', 'CategoryStoreAction', 500)
81
+ }
64
82
  },
65
83
  })
@@ -1,7 +1,9 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { db } from '@stacksjs/database'
4
+ import { transaction } from '@stacksjs/orm'
4
5
  import { response } from '@stacksjs/router'
6
+ import { dashboardOperationalError } from '../dashboard-response'
5
7
  import { rowExists, rowId } from './content-input'
6
8
 
7
9
  /**
@@ -20,11 +22,22 @@ export default new Action({
20
22
  if (!id)
21
23
  return response.json({ message: 'A valid comment id is required.' }, 422)
22
24
 
23
- if (!await rowExists('comments', id))
24
- return response.json({ message: 'Comment not found.' }, 404)
25
+ try {
26
+ const deleted = await transaction(async (rawTrx) => {
27
+ const trx = rawTrx as unknown as typeof db
28
+ if (!await rowExists('comments', id, trx))
29
+ return false
30
+ await trx.deleteFrom('comments').where('id', '=', id).execute()
31
+ return true
32
+ })
25
33
 
26
- await db.deleteFrom('comments').where('id', '=', id).execute()
34
+ if (!deleted)
35
+ return response.json({ message: 'Comment not found.' }, 404)
27
36
 
28
- return response.json({ message: 'Comment deleted.', id })
37
+ return response.json({ message: 'Comment deleted.', id })
38
+ }
39
+ catch (error) {
40
+ return dashboardOperationalError(error, 'Comment could not be deleted.', 'CommentDestroyAction', 500)
41
+ }
29
42
  },
30
43
  })
@@ -1,5 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
2
  import { db } from '@stacksjs/database'
3
+ import { dashboardOperationalError } from '../dashboard-response'
3
4
  import { normalizeCommentStatus } from './comment-input'
4
5
 
5
6
  interface CommentRow {
@@ -33,24 +34,29 @@ export default new Action({
33
34
  method: 'GET',
34
35
  apiResponse: true,
35
36
  async handle() {
36
- const rows = await db
37
- .selectFrom('comments')
38
- .selectAll()
39
- .orderBy('created_at', 'desc')
40
- .execute() as unknown as CommentRow[]
37
+ try {
38
+ const rows = await db
39
+ .selectFrom('comments')
40
+ .selectAll()
41
+ .orderBy('created_at', 'desc')
42
+ .execute() as unknown as CommentRow[]
41
43
 
42
- const comments = rows.map(row => ({
43
- id: Number(row.id),
44
- author_name: String(row.author_name || ''),
45
- author_email: String(row.author_email || ''),
46
- content: String(row.content || row.body || ''),
47
- post_title: String(row.post_title || ''),
48
- status: normalizeCommentStatus(row.status),
49
- is_approved: Boolean(row.is_approved),
50
- created_at: row.created_at || null,
51
- updated_at: row.updated_at || null,
52
- }))
44
+ const comments = rows.map(row => ({
45
+ id: Number(row.id),
46
+ author_name: String(row.author_name || ''),
47
+ author_email: String(row.author_email || ''),
48
+ content: String(row.content || row.body || ''),
49
+ post_title: String(row.post_title || ''),
50
+ status: normalizeCommentStatus(row.status),
51
+ is_approved: Boolean(row.is_approved),
52
+ created_at: row.created_at || null,
53
+ updated_at: row.updated_at || null,
54
+ }))
53
55
 
54
- return { comments }
56
+ return { comments }
57
+ }
58
+ catch (error) {
59
+ return dashboardOperationalError(error, 'Comments could not be loaded.', 'CommentIndexAction')
60
+ }
55
61
  },
56
62
  })
@@ -1,7 +1,9 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { db } from '@stacksjs/database'
4
+ import { transaction } from '@stacksjs/orm'
4
5
  import { response } from '@stacksjs/router'
6
+ import { dashboardOperationalError } from '../dashboard-response'
5
7
  import { COMMENT_STATUSES, parseCommentStatus } from './comment-input'
6
8
  import { findRow, rowExists, rowId, timestamp } from './content-input'
7
9
 
@@ -30,19 +32,35 @@ export default new Action({
30
32
  if (!status)
31
33
  return response.json({ message: `Status must be one of: ${COMMENT_STATUSES.join(', ')}.` }, 422)
32
34
 
33
- if (!await rowExists('comments', id))
34
- return response.json({ message: 'Comment not found.' }, 404)
35
+ try {
36
+ const comment = await transaction(async (rawTrx) => {
37
+ const trx = rawTrx as unknown as typeof db
38
+ if (!await rowExists('comments', id, trx))
39
+ return null
35
40
 
36
- await db
37
- .updateTable('comments')
38
- .set({
39
- status,
40
- is_approved: status === 'approved' ? 1 : 0,
41
- updated_at: timestamp(),
42
- } as any)
43
- .where('id', '=', id)
44
- .execute()
41
+ await trx
42
+ .updateTable('comments')
43
+ .set({
44
+ status,
45
+ is_approved: status === 'approved' ? 1 : 0,
46
+ updated_at: timestamp(),
47
+ } as any)
48
+ .where('id', '=', id)
49
+ .execute()
45
50
 
46
- return response.json(await findRow('comments', id))
51
+ const updated = await findRow('comments', id, trx)
52
+ if (!updated)
53
+ throw new Error('Updated comment could not be loaded.')
54
+ return updated
55
+ })
56
+
57
+ if (!comment)
58
+ return response.json({ message: 'Comment not found.' }, 404)
59
+
60
+ return response.json(comment)
61
+ }
62
+ catch (error) {
63
+ return dashboardOperationalError(error, 'Comment could not be updated.', 'CommentUpdateAction', 500)
64
+ }
47
65
  },
48
66
  })
@@ -1,6 +1,7 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { db } from '@stacksjs/database'
4
+ import { dashboardOperationalError } from '../dashboard-response'
4
5
 
5
6
  const ALLOWED_RANGES = new Set([1, 7, 30, 90, 365])
6
7
 
@@ -214,42 +215,47 @@ export default new Action({
214
215
  const days = requestedDays(request)
215
216
  const since = threshold(days)
216
217
 
217
- const postQuery = db
218
- .selectFrom('posts')
219
- .select(['id', 'title', 'status', 'views', 'author_id', 'published_at', 'created_at', 'updated_at'])
220
- const pageQuery = db
221
- .selectFrom('pages')
222
- .select(['id', 'title', 'views', 'published_at', 'created_at'])
223
- const commentQuery = db
224
- .selectFrom('comments')
225
- .select(['id', 'author_name', 'author_email', 'content', 'post_id', 'post_title', 'status', 'created_at'])
226
-
227
- const [posts, pages, comments, categories, authors] = await Promise.all([
228
- (since ? postQuery.where('created_at', '>=', since) : postQuery).execute() as unknown as Promise<ContentOverviewPostRow[]>,
229
- (since ? pageQuery.where('created_at', '>=', since) : pageQuery).execute() as unknown as Promise<ContentOverviewPageRow[]>,
230
- (since ? commentQuery.where('created_at', '>=', since) : commentQuery).execute() as unknown as Promise<ContentOverviewCommentRow[]>,
231
- db.selectFrom('categories').select(['id', 'name']).execute() as unknown as Promise<NamedRow[]>,
232
- db.selectFrom('authors').select(['id', 'name']).execute() as unknown as Promise<NamedRow[]>,
233
- ])
234
-
235
- const postIds = posts.map(post => Number(post.id))
236
- const categoryRelations = postIds.length > 0
237
- ? await db
238
- .selectFrom('categorizable_models')
239
- .whereIn('categorizable_id', postIds)
240
- .where('categorizable_type', '=', 'posts')
241
- .select(['categorizable_id as postId', 'category_id as categoryId'])
242
- .execute() as unknown as ContentOverviewRelationRow[]
243
- : []
244
-
245
- return buildContentOverview({
246
- posts,
247
- pages,
248
- comments,
249
- categories,
250
- categoryRelations,
251
- authors,
252
- days,
253
- })
218
+ try {
219
+ const postQuery = db
220
+ .selectFrom('posts')
221
+ .select(['id', 'title', 'status', 'views', 'author_id', 'published_at', 'created_at', 'updated_at'])
222
+ const pageQuery = db
223
+ .selectFrom('pages')
224
+ .select(['id', 'title', 'views', 'published_at', 'created_at'])
225
+ const commentQuery = db
226
+ .selectFrom('comments')
227
+ .select(['id', 'author_name', 'author_email', 'content', 'post_id', 'post_title', 'status', 'created_at'])
228
+
229
+ const [posts, pages, comments, categories, authors] = await Promise.all([
230
+ (since ? postQuery.where('created_at', '>=', since) : postQuery).execute() as unknown as Promise<ContentOverviewPostRow[]>,
231
+ (since ? pageQuery.where('created_at', '>=', since) : pageQuery).execute() as unknown as Promise<ContentOverviewPageRow[]>,
232
+ (since ? commentQuery.where('created_at', '>=', since) : commentQuery).execute() as unknown as Promise<ContentOverviewCommentRow[]>,
233
+ db.selectFrom('categories').select(['id', 'name']).execute() as unknown as Promise<NamedRow[]>,
234
+ db.selectFrom('authors').select(['id', 'name']).execute() as unknown as Promise<NamedRow[]>,
235
+ ])
236
+
237
+ const postIds = posts.map(post => Number(post.id))
238
+ const categoryRelations = postIds.length > 0
239
+ ? await db
240
+ .selectFrom('categorizable_models')
241
+ .whereIn('categorizable_id', postIds)
242
+ .where('categorizable_type', '=', 'posts')
243
+ .select(['categorizable_id as postId', 'category_id as categoryId'])
244
+ .execute() as unknown as ContentOverviewRelationRow[]
245
+ : []
246
+
247
+ return buildContentOverview({
248
+ posts,
249
+ pages,
250
+ comments,
251
+ categories,
252
+ categoryRelations,
253
+ authors,
254
+ days,
255
+ })
256
+ }
257
+ catch (error) {
258
+ return dashboardOperationalError(error, 'Content overview could not be loaded.', 'ContentDashboardAction')
259
+ }
254
260
  },
255
261
  })
@@ -1,7 +1,9 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { db } from '@stacksjs/database'
4
+ import { transaction } from '@stacksjs/orm'
4
5
  import { response } from '@stacksjs/router'
6
+ import { dashboardOperationalError } from '../dashboard-response'
5
7
  import { rowExists, rowId } from './content-input'
6
8
 
7
9
  /** `DELETE /api/dashboard/pages/{id}` — deletes a CMS page from the dashboard. */
@@ -15,11 +17,22 @@ export default new Action({
15
17
  if (!id)
16
18
  return response.json({ message: 'A valid page id is required.' }, 422)
17
19
 
18
- if (!await rowExists('pages', id))
19
- return response.json({ message: 'Page not found.' }, 404)
20
+ try {
21
+ const deleted = await transaction(async (rawTrx) => {
22
+ const trx = rawTrx as unknown as typeof db
23
+ if (!await rowExists('pages', id, trx))
24
+ return false
25
+ await trx.deleteFrom('pages').where('id', '=', id).execute()
26
+ return true
27
+ })
20
28
 
21
- await db.deleteFrom('pages').where('id', '=', id).execute()
29
+ if (!deleted)
30
+ return response.json({ message: 'Page not found.' }, 404)
22
31
 
23
- return response.json({ message: 'Page deleted.', id })
32
+ return response.json({ message: 'Page deleted.', id })
33
+ }
34
+ catch (error) {
35
+ return dashboardOperationalError(error, 'Page could not be deleted.', 'PageDestroyAction', 500)
36
+ }
24
37
  },
25
38
  })
@@ -1,5 +1,6 @@
1
1
  import { Action } from '@stacksjs/actions'
2
2
  import { db } from '@stacksjs/database'
3
+ import { dashboardOperationalError } from '../dashboard-response'
3
4
 
4
5
  interface PageRow {
5
6
  id: number
@@ -30,24 +31,29 @@ export default new Action({
30
31
  method: 'GET',
31
32
  apiResponse: true,
32
33
  async handle() {
33
- const rows = await db
34
- .selectFrom('pages')
35
- .selectAll()
36
- .orderBy('created_at', 'desc')
37
- .execute() as unknown as PageRow[]
34
+ try {
35
+ const rows = await db
36
+ .selectFrom('pages')
37
+ .selectAll()
38
+ .orderBy('created_at', 'desc')
39
+ .execute() as unknown as PageRow[]
38
40
 
39
- const pages = rows.map(row => ({
40
- id: Number(row.id),
41
- title: String(row.title || ''),
42
- template: String(row.template || 'default'),
43
- views: Number(row.views || 0),
44
- conversions: Number(row.conversions || 0),
45
- published_at: row.published_at || null,
46
- author_id: row.author_id ?? null,
47
- created_at: row.created_at || null,
48
- updated_at: row.updated_at || null,
49
- }))
41
+ const pages = rows.map(row => ({
42
+ id: Number(row.id),
43
+ title: String(row.title || ''),
44
+ template: String(row.template || 'default'),
45
+ views: Number(row.views || 0),
46
+ conversions: Number(row.conversions || 0),
47
+ published_at: row.published_at || null,
48
+ author_id: row.author_id ?? null,
49
+ created_at: row.created_at || null,
50
+ updated_at: row.updated_at || null,
51
+ }))
50
52
 
51
- return { pages }
53
+ return { pages }
54
+ }
55
+ catch (error) {
56
+ return dashboardOperationalError(error, 'Pages could not be loaded.', 'PageIndexAction')
57
+ }
52
58
  },
53
59
  })
@@ -1,8 +1,10 @@
1
1
  import type { RequestInstance } from '@stacksjs/types'
2
2
  import { Action } from '@stacksjs/actions'
3
3
  import { db } from '@stacksjs/database'
4
+ import { transaction } from '@stacksjs/orm'
4
5
  import { response } from '@stacksjs/router'
5
6
  import { randomUUIDv7 } from 'bun'
7
+ import { dashboardOperationalError } from '../dashboard-response'
6
8
  import { findRow, insertedId, timestamp } from './content-input'
7
9
  import { parsePageInput, parsePublished } from './page-input'
8
10
 
@@ -22,27 +24,40 @@ export default new Action({
22
24
  if ('message' in input)
23
25
  return response.json({ message: input.message }, 422)
24
26
 
25
- const now = timestamp()
27
+ try {
28
+ const page = await transaction(async (rawTrx) => {
29
+ const trx = rawTrx as unknown as typeof db
30
+ const now = timestamp()
26
31
 
27
- const result = await db
28
- .insertInto('pages')
29
- .values({
30
- uuid: randomUUIDv7(),
31
- title: input.data.title,
32
- template: input.data.template,
33
- views: 0,
34
- conversions: 0,
35
- published_at: parsePublished(request.get('published')) ? now : null,
36
- created_at: now,
37
- updated_at: now,
38
- } as any)
39
- .executeTakeFirst()
32
+ const result = await trx
33
+ .insertInto('pages')
34
+ .values({
35
+ uuid: randomUUIDv7(),
36
+ title: input.data.title,
37
+ template: input.data.template,
38
+ views: 0,
39
+ conversions: 0,
40
+ published_at: parsePublished(request.get('published')) ? now : null,
41
+ created_at: now,
42
+ updated_at: now,
43
+ } as any)
44
+ .executeTakeFirst()
40
45
 
41
- const id = insertedId(result)
46
+ const id = insertedId(result)
42
47
 
43
- if (!id)
44
- return response.json({ message: 'Could not create page.' }, 500)
48
+ if (!id)
49
+ throw new Error('Page insert did not return an id.')
45
50
 
46
- return response.json(await findRow('pages', id), 201)
51
+ const created = await findRow('pages', id, trx)
52
+ if (!created)
53
+ throw new Error('Created page could not be loaded.')
54
+ return created
55
+ })
56
+
57
+ return response.json(page, 201)
58
+ }
59
+ catch (error) {
60
+ return dashboardOperationalError(error, 'Page could not be created.', 'PageStoreAction', 500)
61
+ }
47
62
  },
48
63
  })