@stacksjs/defaults 0.70.355 → 0.70.357

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 (180) hide show
  1. package/ai/skills/stacks-email/SKILL.md +38 -1
  2. package/ai/skills/stacks-orm/SKILL.md +6 -0
  3. package/app/Actions/ContactAction.ts +2 -2
  4. package/app/Actions/Dashboard/Analytics/EventAnalyticsAction.ts +2 -3
  5. package/app/Actions/Dashboard/Analytics/MarketingAnalyticsAction.ts +2 -3
  6. package/app/Actions/Dashboard/Analytics/SalesAnalyticsAction.ts +2 -3
  7. package/app/Actions/Dashboard/Analytics/WebAnalyticsAction.ts +9 -3
  8. package/app/Actions/Dashboard/Auth/MeAction.ts +4 -7
  9. package/app/Actions/Dashboard/Buddy/BuddyChatAction.ts +2 -3
  10. package/app/Actions/Dashboard/Cloud/CloudIndexAction.ts +8 -1
  11. package/app/Actions/Dashboard/Cloud/ServerlessIndexAction.ts +15 -8
  12. package/app/Actions/Dashboard/Commerce/CommerceCategoriesAction.ts +2 -4
  13. package/app/Actions/Dashboard/Commerce/CommerceCouponsAction.ts +2 -4
  14. package/app/Actions/Dashboard/Commerce/CommerceCustomersAction.ts +2 -4
  15. package/app/Actions/Dashboard/Commerce/CommerceDashboardAction.ts +2 -4
  16. package/app/Actions/Dashboard/Commerce/CommerceDeliveryAction.ts +2 -4
  17. package/app/Actions/Dashboard/Commerce/CommerceGiftCardsAction.ts +2 -4
  18. package/app/Actions/Dashboard/Commerce/CommerceOrdersAction.ts +2 -4
  19. package/app/Actions/Dashboard/Commerce/CommercePaymentsAction.ts +2 -4
  20. package/app/Actions/Dashboard/Commerce/CommercePosAction.ts +2 -4
  21. package/app/Actions/Dashboard/Commerce/CommercePosCheckoutAction.ts +75 -61
  22. package/app/Actions/Dashboard/Commerce/CommercePrintDevicesAction.ts +2 -4
  23. package/app/Actions/Dashboard/Commerce/CommercePrintLogsAction.ts +2 -4
  24. package/app/Actions/Dashboard/Commerce/CommerceProductDetailAction.ts +3 -4
  25. package/app/Actions/Dashboard/Commerce/CommerceProductsAction.ts +2 -4
  26. package/app/Actions/Dashboard/Commerce/CommerceTaxesAction.ts +2 -4
  27. package/app/Actions/Dashboard/Commerce/DeliveryRouteIndexAction.ts +2 -4
  28. package/app/Actions/Dashboard/Commerce/DigitalDeliveryIndexAction.ts +2 -4
  29. package/app/Actions/Dashboard/Commerce/DriverIndexAction.ts +2 -4
  30. package/app/Actions/Dashboard/Commerce/LicenseKeyIndexAction.ts +2 -4
  31. package/app/Actions/Dashboard/Commerce/LicenseKeyOptionsAction.ts +2 -4
  32. package/app/Actions/Dashboard/Commerce/ManufacturerIndexAction.ts +2 -4
  33. package/app/Actions/Dashboard/Commerce/ProductUnitIndexAction.ts +2 -4
  34. package/app/Actions/Dashboard/Commerce/ProductVariantIndexAction.ts +2 -4
  35. package/app/Actions/Dashboard/Commerce/ProductWaitlistIndexAction.ts +2 -4
  36. package/app/Actions/Dashboard/Commerce/RestaurantWaitlistIndexAction.ts +2 -4
  37. package/app/Actions/Dashboard/Commerce/ReviewIndexAction.ts +2 -4
  38. package/app/Actions/Dashboard/Commerce/ShippingMethodIndexAction.ts +2 -4
  39. package/app/Actions/Dashboard/Commerce/ShippingRateIndexAction.ts +2 -4
  40. package/app/Actions/Dashboard/Commerce/ShippingZoneIndexAction.ts +2 -4
  41. package/app/Actions/Dashboard/Content/AuthorDestroyAction.ts +23 -9
  42. package/app/Actions/Dashboard/Content/AuthorIndexAction.ts +32 -26
  43. package/app/Actions/Dashboard/Content/AuthorStoreAction.ts +44 -24
  44. package/app/Actions/Dashboard/Content/AuthorUpdateAction.ts +48 -26
  45. package/app/Actions/Dashboard/Content/CategoryDestroyAction.ts +27 -8
  46. package/app/Actions/Dashboard/Content/CategoryIndexAction.ts +21 -15
  47. package/app/Actions/Dashboard/Content/CategoryStoreAction.ts +47 -29
  48. package/app/Actions/Dashboard/Content/CommentDestroyAction.ts +17 -4
  49. package/app/Actions/Dashboard/Content/CommentIndexAction.ts +23 -17
  50. package/app/Actions/Dashboard/Content/CommentUpdateAction.ts +30 -12
  51. package/app/Actions/Dashboard/Content/ContentDashboardAction.ts +43 -37
  52. package/app/Actions/Dashboard/Content/PageDestroyAction.ts +17 -4
  53. package/app/Actions/Dashboard/Content/PageIndexAction.ts +23 -17
  54. package/app/Actions/Dashboard/Content/PageStoreAction.ts +33 -18
  55. package/app/Actions/Dashboard/Content/PageUpdateAction.ts +40 -22
  56. package/app/Actions/Dashboard/Content/PostDestroyAction.ts +24 -11
  57. package/app/Actions/Dashboard/Content/PostIndexAction.ts +55 -49
  58. package/app/Actions/Dashboard/Content/PostStoreAction.ts +43 -33
  59. package/app/Actions/Dashboard/Content/PostUpdateAction.ts +46 -34
  60. package/app/Actions/Dashboard/Content/TagDestroyAction.ts +27 -8
  61. package/app/Actions/Dashboard/Content/TagIndexAction.ts +31 -25
  62. package/app/Actions/Dashboard/Content/TagStoreAction.ts +44 -23
  63. package/app/Actions/Dashboard/Content/content-input.ts +4 -4
  64. package/app/Actions/Dashboard/Content/post-input.ts +60 -8
  65. package/app/Actions/Dashboard/DashboardActivityAction.ts +2 -5
  66. package/app/Actions/Dashboard/Data/ActivityIndexAction.ts +2 -5
  67. package/app/Actions/Dashboard/Data/SubscriberIndexAction.ts +2 -5
  68. package/app/Actions/Dashboard/Data/TeamIndexAction.ts +2 -5
  69. package/app/Actions/Dashboard/Data/UserIndexAction.ts +2 -5
  70. package/app/Actions/Dashboard/Deployments/CreateDeployment.ts +23 -19
  71. package/app/Actions/Dashboard/Deployments/GetAverageDeploymentTime.ts +8 -2
  72. package/app/Actions/Dashboard/Deployments/GetDeployScript.ts +3 -6
  73. package/app/Actions/Dashboard/Deployments/GetDeployment.ts +4 -5
  74. package/app/Actions/Dashboard/Deployments/GetDeploymentCount.ts +7 -1
  75. package/app/Actions/Dashboard/Deployments/GetDeploymentLiveTerminalOutput.ts +10 -9
  76. package/app/Actions/Dashboard/Deployments/GetDeployments.ts +2 -4
  77. package/app/Actions/Dashboard/Deployments/GetRecentDeployments.ts +8 -2
  78. package/app/Actions/Dashboard/Deployments/UpdateDeployScript.ts +18 -5
  79. package/app/Actions/Dashboard/Email/CapturedMailIndexAction.ts +2 -4
  80. package/app/Actions/Dashboard/Email/CapturedMailShowAction.ts +3 -4
  81. package/app/Actions/Dashboard/Infrastructure/InsightsAction.ts +15 -14
  82. package/app/Actions/Dashboard/Infrastructure/LogIndexAction.ts +2 -8
  83. package/app/Actions/Dashboard/Jobs/JobIndexAction.ts +2 -4
  84. package/app/Actions/Dashboard/Jobs/JobShowAction.ts +13 -8
  85. package/app/Actions/Dashboard/Jobs/JobStatsAction.ts +2 -4
  86. package/app/Actions/Dashboard/Kanban/BoardDestroyAction.ts +2 -3
  87. package/app/Actions/Dashboard/Kanban/BoardShowAction.ts +2 -3
  88. package/app/Actions/Dashboard/Kanban/BoardStoreAction.ts +2 -3
  89. package/app/Actions/Dashboard/Kanban/BoardUpdateAction.ts +2 -3
  90. package/app/Actions/Dashboard/Kanban/BoardsIndexAction.ts +2 -3
  91. package/app/Actions/Dashboard/Kanban/BoardsReorderAction.ts +2 -3
  92. package/app/Actions/Dashboard/Kanban/CardAssigneesSyncAction.ts +2 -3
  93. package/app/Actions/Dashboard/Kanban/CardCommentDestroyAction.ts +2 -3
  94. package/app/Actions/Dashboard/Kanban/CardCommentStoreAction.ts +2 -3
  95. package/app/Actions/Dashboard/Kanban/CardCommentUpdateAction.ts +2 -3
  96. package/app/Actions/Dashboard/Kanban/CardDestroyAction.ts +2 -3
  97. package/app/Actions/Dashboard/Kanban/CardLabelsSyncAction.ts +2 -3
  98. package/app/Actions/Dashboard/Kanban/CardShowAction.ts +2 -3
  99. package/app/Actions/Dashboard/Kanban/CardStoreAction.ts +2 -3
  100. package/app/Actions/Dashboard/Kanban/CardUpdateAction.ts +2 -3
  101. package/app/Actions/Dashboard/Kanban/CardsReorderAction.ts +2 -3
  102. package/app/Actions/Dashboard/Kanban/ColumnDestroyAction.ts +2 -3
  103. package/app/Actions/Dashboard/Kanban/ColumnStoreAction.ts +2 -3
  104. package/app/Actions/Dashboard/Kanban/ColumnUpdateAction.ts +2 -3
  105. package/app/Actions/Dashboard/Kanban/ColumnsReorderAction.ts +2 -3
  106. package/app/Actions/Dashboard/Kanban/LabelDestroyAction.ts +2 -3
  107. package/app/Actions/Dashboard/Kanban/LabelStoreAction.ts +2 -3
  108. package/app/Actions/Dashboard/Kanban/LabelUpdateAction.ts +2 -3
  109. package/app/Actions/Dashboard/Kanban/UsersListAction.ts +2 -3
  110. package/app/Actions/Dashboard/Kanban/kanban-response.ts +5 -0
  111. package/app/Actions/Dashboard/Library/CreateComponent.ts +15 -6
  112. package/app/Actions/Dashboard/Library/CreateFunction.ts +15 -6
  113. package/app/Actions/Dashboard/Library/DependencyIndexAction.ts +88 -77
  114. package/app/Actions/Dashboard/Library/GetAverageReleaseTime.ts +21 -15
  115. package/app/Actions/Dashboard/Library/GetComponents.ts +14 -7
  116. package/app/Actions/Dashboard/Library/GetDownloadCount.ts +14 -5
  117. package/app/Actions/Dashboard/Library/GetFunctions.ts +12 -5
  118. package/app/Actions/Dashboard/Library/GetReleaseCount.ts +14 -5
  119. package/app/Actions/Dashboard/Library/GetReleases.ts +17 -10
  120. package/app/Actions/Dashboard/Library/PackageIndexAction.ts +14 -7
  121. package/app/Actions/Dashboard/Models/ModelDestroyAction.ts +7 -5
  122. package/app/Actions/Dashboard/Models/ModelShowAction.ts +82 -174
  123. package/app/Actions/Dashboard/Models/ModelStoreAction.ts +6 -4
  124. package/app/Actions/Dashboard/Models/ModelUpdateAction.ts +9 -7
  125. package/app/Actions/Dashboard/Models/ModelsIndexAction.ts +47 -17
  126. package/app/Actions/Dashboard/Models/model-write.ts +20 -5
  127. package/app/Actions/Dashboard/Notifications/NotificationDeliveryHistoryAction.ts +46 -36
  128. package/app/Actions/Dashboard/Notifications/NotificationDeliveryIndexAction.ts +14 -6
  129. package/app/Actions/Dashboard/Notifications/NotificationDeliveryOverviewAction.ts +96 -84
  130. package/app/Actions/Dashboard/Notifications/NotificationDeliveryRetryAction.ts +34 -21
  131. package/app/Actions/Dashboard/Queries/QueryDashboardAction.ts +2 -4
  132. package/app/Actions/Dashboard/Queries/QueryShowAction.ts +2 -3
  133. package/app/Actions/Dashboard/Queue/QueueStatsAction.ts +2 -4
  134. package/app/Actions/Dashboard/Queue/QueueWorkersAction.ts +2 -4
  135. package/app/Actions/Dashboard/Rbac/PermissionDestroyAction.ts +2 -2
  136. package/app/Actions/Dashboard/Rbac/PermissionStoreAction.ts +2 -2
  137. package/app/Actions/Dashboard/Rbac/PermissionsIndexAction.ts +2 -3
  138. package/app/Actions/Dashboard/Rbac/RoleDestroyAction.ts +2 -2
  139. package/app/Actions/Dashboard/Rbac/RolePermissionsShowAction.ts +2 -2
  140. package/app/Actions/Dashboard/Rbac/RolePermissionsSyncAction.ts +2 -6
  141. package/app/Actions/Dashboard/Rbac/RoleStoreAction.ts +2 -2
  142. package/app/Actions/Dashboard/Rbac/RolesIndexAction.ts +4 -6
  143. package/app/Actions/Dashboard/Rbac/UserRolesShowAction.ts +2 -2
  144. package/app/Actions/Dashboard/Rbac/UserRolesSyncAction.ts +2 -9
  145. package/app/Actions/Dashboard/Rbac/UsersListAction.ts +2 -3
  146. package/app/Actions/Dashboard/Rbac/rbac-response.ts +10 -0
  147. package/app/Actions/Dashboard/Realtime/RealtimeStatsAction.ts +2 -4
  148. package/app/Actions/Dashboard/Search/GlobalSearchAction.ts +68 -55
  149. package/app/Actions/Dashboard/Teams/TeamInvitationDestroyAction.ts +34 -16
  150. package/app/Actions/Dashboard/Teams/TeamInvitationResendAction.ts +52 -29
  151. package/app/Actions/Dashboard/Teams/TeamInviteAction.ts +102 -71
  152. package/app/Actions/Dashboard/Teams/TeamMemberDestroyAction.ts +36 -17
  153. package/app/Actions/Dashboard/Teams/TeamMemberUpdateAction.ts +39 -26
  154. package/app/Actions/Dashboard/Teams/TeamPeopleIndexAction.ts +71 -58
  155. package/app/Actions/Dashboard/Teams/team-invitation-delivery.ts +29 -7
  156. package/app/Actions/Dashboard/Teams/team-member-count.ts +4 -2
  157. package/app/Actions/Dashboard/Teams/team-records.test.ts +2 -0
  158. package/app/Actions/Dashboard/Teams/team-records.ts +2 -0
  159. package/app/Actions/Dashboard/dashboard-response.ts +16 -0
  160. package/app/Actions/SendWelcomeEmail.ts +1 -1
  161. package/app/Actions/Teams/AcceptInvitationAction.ts +74 -51
  162. package/app/Actions/Teams/ShowInvitationAction.ts +21 -14
  163. package/app/Actions/Teams/team-response.ts +23 -0
  164. package/app/Mail/OrderConfirmation.ts +1 -1
  165. package/app/Mail/SubscriptionConfirmation.ts +1 -1
  166. package/app/Mail/TeamInvitation.ts +1 -1
  167. package/app/Models/EmailIdempotency.ts +64 -0
  168. package/app/Models/EmailSuppression.ts +52 -0
  169. package/app/Models/EmailWebhookEvent.ts +51 -0
  170. package/app/Models/TeamInvitation.ts +13 -0
  171. package/functions/data.ts +0 -1
  172. package/functions/jobs.ts +0 -1
  173. package/ide/vscode/package.json +1 -1
  174. package/package.json +1 -1
  175. package/resources/components/Dashboard/Data/DataRecords.stx +0 -4
  176. package/resources/components/Dashboard/Jobs/JobDetail.stx +3 -2
  177. package/resources/components/Dashboard/Models/ModelRecordsDashboard.stx +7 -13
  178. package/resources/components/Dashboard/Models/ModelsOverview.stx +1 -1
  179. package/views/dashboard/.discovered-models.json +30 -0
  180. package/views/dashboard/stores/auth.ts +15 -14
@@ -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
  })
@@ -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 { findRow, rowExists, rowId, timestamp } from './content-input'
6
8
  import { parsePageInput, parsePublished } from './page-input'
7
9
 
@@ -15,32 +17,48 @@ 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
-
21
20
  const input = parsePageInput(request)
22
21
 
23
22
  if ('message' in input)
24
23
  return response.json({ message: input.message }, 422)
25
24
 
26
- const current = await db
27
- .selectFrom('pages')
28
- .select(['published_at'])
29
- .where('id', '=', id)
30
- .executeTakeFirst()
31
- const published = parsePublished(request.get('published'))
32
-
33
- await db
34
- .updateTable('pages')
35
- .set({
36
- title: input.data.title,
37
- template: input.data.template,
38
- published_at: published ? current?.published_at || timestamp() : null,
39
- updated_at: timestamp(),
40
- } as any)
41
- .where('id', '=', id)
42
- .execute()
43
-
44
- return response.json(await findRow('pages', id))
25
+ try {
26
+ const page = await transaction(async (rawTrx) => {
27
+ const trx = rawTrx as unknown as typeof db
28
+ if (!await rowExists('pages', id, trx))
29
+ return null
30
+
31
+ const current = await trx
32
+ .selectFrom('pages')
33
+ .select(['published_at'])
34
+ .where('id', '=', id)
35
+ .executeTakeFirst()
36
+ const published = parsePublished(request.get('published'))
37
+
38
+ await trx
39
+ .updateTable('pages')
40
+ .set({
41
+ title: input.data.title,
42
+ template: input.data.template,
43
+ published_at: published ? current?.published_at || timestamp() : null,
44
+ updated_at: timestamp(),
45
+ } as any)
46
+ .where('id', '=', id)
47
+ .execute()
48
+
49
+ const updated = await findRow('pages', id, trx)
50
+ if (!updated)
51
+ throw new Error('Updated page could not be loaded.')
52
+ return updated
53
+ })
54
+
55
+ if (!page)
56
+ return response.json({ message: 'Page not found.' }, 404)
57
+
58
+ return response.json(page)
59
+ }
60
+ catch (error) {
61
+ return dashboardOperationalError(error, 'Page could not be updated.', 'PageUpdateAction', 500)
62
+ }
45
63
  },
46
64
  })
@@ -1,12 +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'
7
+ import { detachPostRelations } from './post-input'
4
8
 
5
9
  /**
6
10
  * `DELETE /api/dashboard/posts/{id}` — deletes a CMS post from the dashboard.
7
11
  *
8
- * Detaches the native category and tag relations before removing the post so
9
- * no pivot rows are orphaned.
12
+ * Detaches the model-declared category and tag pivots before removing the post,
13
+ * with all three writes committed by the same transaction.
10
14
  */
11
15
  export default new Action({
12
16
  name: 'PostDestroyAction',
@@ -18,17 +22,26 @@ export default new Action({
18
22
  if (!Number.isInteger(id) || id <= 0)
19
23
  return response.json({ message: 'A valid post id is required.' }, 422)
20
24
 
21
- const post = await Post.find(id)
25
+ try {
26
+ const deleted = await transaction(async (rawTrx) => {
27
+ const trx = rawTrx as unknown as typeof db
28
+ const post = await trx.selectFrom('posts').select(['id']).where('id', '=', id).executeTakeFirst()
22
29
 
23
- if (!post)
24
- return response.json({ message: 'Post not found.' }, 404)
30
+ if (!post)
31
+ return false
25
32
 
26
- await Promise.all([
27
- post.categories().detach(),
28
- post.tags().detach(),
29
- ])
30
- await post.delete()
33
+ await detachPostRelations(trx, id)
34
+ await trx.deleteFrom('posts').where('id', '=', id).execute()
35
+ return true
36
+ })
31
37
 
32
- return response.json({ message: 'Post deleted.', id })
38
+ if (!deleted)
39
+ return response.json({ message: 'Post not found.' }, 404)
40
+
41
+ return response.json({ message: 'Post deleted.', id })
42
+ }
43
+ catch (error) {
44
+ return dashboardOperationalError(error, 'Post could not be deleted.', 'PostDestroyAction', 500)
45
+ }
33
46
  },
34
47
  })
@@ -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 PostRow {
5
6
  id: number
@@ -55,8 +56,8 @@ function counts(posts: Array<{ status: string }>) {
55
56
  * `GET /api/dashboard/posts` — backs `views/dashboard/content/posts/index.stx`.
56
57
  *
57
58
  * Uses one bounded query per persisted dataset and groups pivot ids in maps,
58
- * avoiding relation N+1 queries while still keeping all writes on the native
59
- * model relations.
59
+ * avoiding relation N+1 queries. Writes use the same model-declared pivots on
60
+ * one native database transaction.
60
61
  *
61
62
  * There is deliberately no mock-data fallback. This action used to catch every
62
63
  * error and serve placeholder rows, which made a page that was 404ing against
@@ -69,56 +70,61 @@ export default new Action({
69
70
  method: 'GET',
70
71
  apiResponse: true,
71
72
  async handle() {
72
- const rows = await db
73
- .selectFrom('posts')
74
- .selectAll()
75
- .orderBy('created_at', 'desc')
76
- .execute() as unknown as PostRow[]
73
+ try {
74
+ const rows = await db
75
+ .selectFrom('posts')
76
+ .selectAll()
77
+ .orderBy('created_at', 'desc')
78
+ .execute() as unknown as PostRow[]
77
79
 
78
- const posts = rows.map(row => ({
79
- id: Number(row.id),
80
- title: String(row.title || ''),
81
- excerpt: String(row.excerpt || ''),
82
- content: String(row.content || ''),
83
- poster: String(row.poster || ''),
84
- status: normalizeStatus(row.status),
85
- views: Number(row.views || 0),
86
- published_at: row.published_at || null,
87
- created_at: row.created_at || null,
88
- updated_at: row.updated_at || null,
89
- author_id: row.author_id ?? null,
90
- featured: Boolean(row.is_featured),
91
- }))
80
+ const posts = rows.map(row => ({
81
+ id: Number(row.id),
82
+ title: String(row.title || ''),
83
+ excerpt: String(row.excerpt || ''),
84
+ content: String(row.content || ''),
85
+ poster: String(row.poster || ''),
86
+ status: normalizeStatus(row.status),
87
+ views: Number(row.views || 0),
88
+ published_at: row.published_at || null,
89
+ created_at: row.created_at || null,
90
+ updated_at: row.updated_at || null,
91
+ author_id: row.author_id ?? null,
92
+ featured: Boolean(row.is_featured),
93
+ }))
92
94
 
93
- const postIds = posts.map(post => post.id)
94
- const [categories, tags, authors, categoryRelations, tagRelations] = await Promise.all([
95
- db.selectFrom('categories').select(['id', 'name', 'slug']).orderBy('name').execute(),
96
- db.selectFrom('tags').select(['id', 'name', 'slug']).orderBy('name').execute(),
97
- db.selectFrom('authors').select(['id', 'name', 'email']).orderBy('name').execute(),
98
- postIds.length
99
- ? db.selectFrom('categorizable_models')
100
- .whereIn('categorizable_id', postIds)
101
- .where('categorizable_type', '=', 'posts')
102
- .select(['category_id as relatedId', 'categorizable_id as postId'])
103
- .execute() as unknown as Promise<PivotRow[]>
104
- : Promise.resolve([]),
105
- postIds.length
106
- ? db.selectFrom('taggable_models')
107
- .whereIn('taggable_id', postIds)
108
- .where('taggable_type', '=', 'posts')
109
- .select(['tag_id as relatedId', 'taggable_id as postId'])
110
- .execute() as unknown as Promise<PivotRow[]>
111
- : Promise.resolve([]),
112
- ])
95
+ const postIds = posts.map(post => post.id)
96
+ const [categories, tags, authors, categoryRelations, tagRelations] = await Promise.all([
97
+ db.selectFrom('categories').select(['id', 'name', 'slug']).orderBy('name').execute(),
98
+ db.selectFrom('tags').select(['id', 'name', 'slug']).orderBy('name').execute(),
99
+ db.selectFrom('authors').select(['id', 'name', 'email']).orderBy('name').execute(),
100
+ postIds.length
101
+ ? db.selectFrom('categorizable_models')
102
+ .whereIn('categorizable_id', postIds)
103
+ .where('categorizable_type', '=', 'posts')
104
+ .select(['category_id as relatedId', 'categorizable_id as postId'])
105
+ .execute() as unknown as Promise<PivotRow[]>
106
+ : Promise.resolve([]),
107
+ postIds.length
108
+ ? db.selectFrom('taggable_models')
109
+ .whereIn('taggable_id', postIds)
110
+ .where('taggable_type', '=', 'posts')
111
+ .select(['tag_id as relatedId', 'taggable_id as postId'])
112
+ .execute() as unknown as Promise<PivotRow[]>
113
+ : Promise.resolve([]),
114
+ ])
113
115
 
114
- const categoryIdsByPost = relatedIdsByPost(categoryRelations)
115
- const tagIdsByPost = relatedIdsByPost(tagRelations)
116
- const withRelations = posts.map(post => ({
117
- ...post,
118
- categoryIds: categoryIdsByPost.get(post.id) || [],
119
- tagIds: tagIdsByPost.get(post.id) || [],
120
- }))
116
+ const categoryIdsByPost = relatedIdsByPost(categoryRelations)
117
+ const tagIdsByPost = relatedIdsByPost(tagRelations)
118
+ const withRelations = posts.map(post => ({
119
+ ...post,
120
+ categoryIds: categoryIdsByPost.get(post.id) || [],
121
+ tagIds: tagIdsByPost.get(post.id) || [],
122
+ }))
121
123
 
122
- return { posts: withRelations, categories, tags, authors, ...counts(posts) }
124
+ return { posts: withRelations, categories, tags, authors, ...counts(posts) }
125
+ }
126
+ catch (error) {
127
+ return dashboardOperationalError(error, 'Posts could not be loaded.', 'PostIndexAction')
128
+ }
123
129
  },
124
130
  })
@@ -1,9 +1,11 @@
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'
6
- import { findPost, insertedId, invalidPostContent, invalidPostReference, postPayload, publishedAtFor, timestamp } from './post-input'
7
+ import { dashboardOperationalError } from '../dashboard-response'
8
+ import { findPost, insertedId, invalidPostContent, invalidPostReference, postPayload, publishedAtFor, syncPostRelations, timestamp } from './post-input'
7
9
 
8
10
  /**
9
11
  * `POST /api/dashboard/posts` — creates a CMS post from the dashboard.
@@ -23,45 +25,53 @@ export default new Action({
23
25
  if (invalidContent)
24
26
  return response.json({ message: invalidContent }, 422)
25
27
 
26
- const invalidReference = await invalidPostReference(payload)
27
- if (invalidReference)
28
- return response.json({ message: invalidReference }, 422)
28
+ try {
29
+ const result = await transaction(async (rawTrx) => {
30
+ const trx = rawTrx as unknown as typeof db
31
+ const invalidReference = await invalidPostReference(payload, trx)
32
+ if (invalidReference)
33
+ return { kind: 'invalid', message: invalidReference } as const
29
34
 
30
- const now = timestamp()
35
+ const now = timestamp()
31
36
 
32
- const result = await db
33
- .insertInto('posts')
34
- .values({
35
- uuid: randomUUIDv7(),
36
- title: payload.title,
37
- excerpt: payload.excerpt,
38
- content: payload.content,
39
- poster: payload.poster,
40
- status: payload.status,
41
- author_id: payload.authorId,
42
- is_featured: payload.featured ? 1 : 0,
43
- views: 0,
44
- published_at: publishedAtFor(payload.status, null, now),
45
- created_at: now,
46
- updated_at: now,
47
- } as any)
48
- .executeTakeFirst()
37
+ const insertResult = await trx
38
+ .insertInto('posts')
39
+ .values({
40
+ uuid: randomUUIDv7(),
41
+ title: payload.title,
42
+ excerpt: payload.excerpt,
43
+ content: payload.content,
44
+ poster: payload.poster,
45
+ status: payload.status,
46
+ author_id: payload.authorId,
47
+ is_featured: payload.featured ? 1 : 0,
48
+ views: 0,
49
+ published_at: publishedAtFor(payload.status, null, now),
50
+ created_at: now,
51
+ updated_at: now,
52
+ } as any)
53
+ .executeTakeFirst()
49
54
 
50
- const id = insertedId(result)
55
+ const id = insertedId(insertResult)
51
56
 
52
- if (!id)
53
- return response.json({ message: 'Could not create post.' }, 500)
57
+ if (!id)
58
+ throw new Error('Post insert did not return an id.')
54
59
 
55
- const post = await Post.find(id)
60
+ await syncPostRelations(trx, id, payload)
56
61
 
57
- if (!post)
58
- return response.json({ message: 'Created post could not be loaded.' }, 500)
62
+ const created = await findPost(id, trx)
63
+ if (!created)
64
+ throw new Error('Created post could not be read back.')
65
+ return { kind: 'created', post: created } as const
66
+ })
59
67
 
60
- await Promise.all([
61
- post.categories().sync(payload.categoryIds),
62
- post.tags().sync(payload.tagIds),
63
- ])
68
+ if (result.kind === 'invalid')
69
+ return response.json({ message: result.message }, 422)
64
70
 
65
- return response.json(await findPost(id), 201)
71
+ return response.json(result.post, 201)
72
+ }
73
+ catch (error) {
74
+ return dashboardOperationalError(error, 'Post could not be created.', 'PostStoreAction', 500)
75
+ }
66
76
  },
67
77
  })