@stacksjs/defaults 0.75.482 → 0.75.484

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 (54) hide show
  1. package/ai/skills/stacks-actions/SKILL.md +1 -1
  2. package/ai/skills/stacks-native/CATALOG.md +4 -4
  3. package/ai/skills/stacks-queue/DURABLE-WORKFLOWS.md +12 -0
  4. package/app/Actions/Dashboard/Content/CommentIndexAction.ts +22 -112
  5. package/app/Actions/Dashboard/Content/PageIndexAction.ts +26 -25
  6. package/app/Actions/Dashboard/Content/PostIndexAction.ts +30 -35
  7. package/app/Actions/Dashboard/Content/PostShowAction.ts +41 -0
  8. package/app/Actions/Dashboard/Content/comment-input.ts +25 -1
  9. package/app/Actions/Dashboard/Content/comment-list.ts +311 -0
  10. package/app/Actions/Dashboard/Content/content-list.ts +69 -0
  11. package/app/Actions/Dashboard/Content/page-list.ts +108 -0
  12. package/app/Actions/Dashboard/Content/post-list.ts +140 -0
  13. package/app/Actions/Dashboard/Jobs/JobIndexAction.ts +12 -24
  14. package/app/Actions/Dashboard/Jobs/job-list.ts +168 -0
  15. package/app/Actions/Dashboard/Jobs/job-records.ts +8 -3
  16. package/app/Actions/Dashboard/Models/ModelShowAction.ts +8 -2
  17. package/app/Actions/Dashboard/Queries/QueryDashboardAction.ts +20 -10
  18. package/app/Actions/Dashboard/Queries/query-dashboard.ts +56 -9
  19. package/app/Actions/Dashboard/Queries/query-list.ts +161 -0
  20. package/app/Actions/Dashboard/Queue/QueueStatsAction.ts +14 -52
  21. package/app/Actions/Dashboard/Queue/queue-stats.ts +69 -0
  22. package/app/Actions/Monitoring/ErrorIndexAction.ts +9 -4
  23. package/app/Actions/Monitoring/error-provider.ts +85 -7
  24. package/functions/api.ts +14 -10
  25. package/functions/commerce/coupons.ts +4 -6
  26. package/functions/commerce/customers.ts +4 -6
  27. package/functions/commerce/gift-cards.ts +4 -2
  28. package/functions/commerce/orders.ts +4 -2
  29. package/functions/commerce/payments.ts +4 -2
  30. package/functions/commerce/products/categories.ts +4 -6
  31. package/functions/commerce/products/manufacturers.ts +4 -6
  32. package/functions/commerce/products/products.ts +4 -6
  33. package/functions/commerce/products/reviews.ts +4 -6
  34. package/functions/commerce/products/units.ts +4 -6
  35. package/functions/commerce/products/variants.ts +4 -6
  36. package/functions/commerce/taxes.ts +4 -2
  37. package/functions/commerce/waitlist/products.ts +4 -6
  38. package/functions/commerce/waitlist/restaurants.ts +4 -6
  39. package/functions/monitoring/errors.ts +77 -49
  40. package/functions/queries.ts +34 -2
  41. package/ide/vscode/package.json +1 -1
  42. package/package.json +2 -2
  43. package/resources/components/Dashboard/Analytics/AnalyticsHub.stx +4 -0
  44. package/resources/components/Dashboard/Auth/MagicLink.stx +6 -1
  45. package/resources/components/Dashboard/Content/CommentsDashboard.stx +90 -12
  46. package/resources/components/Dashboard/Content/PagesDashboard.stx +100 -16
  47. package/resources/components/Dashboard/Content/PostEditorDialog.stx +28 -8
  48. package/resources/components/Dashboard/Content/PostsDashboard.stx +179 -21
  49. package/resources/components/Dashboard/Monitoring/ErrorDashboard.stx +121 -36
  50. package/resources/components/Dashboard/Monitoring/ErrorTable.stx +29 -8
  51. package/resources/components/Dashboard/Queries/QueryDashboard.stx +73 -64
  52. package/resources/components/Dashboard/UI/Table.stx +30 -7
  53. package/routes/dashboard-api.ts +1 -0
  54. package/app/Actions/Buddy/JobsListAction.ts +0 -141
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: stacks-actions
3
- description: Use when working with Stacks server actions - creating actions in app/Actions/, auto-generated API actions from the useApi model trait, the 676 default framework actions (auth, dashboard, commerce, content, deployment, jobs), action request/response handling, or action registration. Covers @stacksjs/actions and storage/framework/defaults/app/Actions/.
3
+ description: Use when working with Stacks server actions - creating actions in app/Actions/, auto-generated API actions from the useApi model trait, the 683 default framework actions (auth, dashboard, commerce, content, deployment, jobs), action request/response handling, or action registration. Covers @stacksjs/actions and storage/framework/defaults/app/Actions/.
4
4
  license: MIT
5
5
  compatibility: Bun >= 1.3.0, TypeScript
6
6
  allowed-tools: Read Edit Write Bash Grep Glob
@@ -11,7 +11,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
11
11
  | Package | Task guidance | Source entry | Retained tests |
12
12
  |---|---|---|---|
13
13
  | `@stacksjs/action-runner` | [`stacks-actions`](../stacks-actions/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/action-runner/src/index.ts) | 1 |
14
- | `@stacksjs/actions` | [`stacks-actions`](../stacks-actions/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/index.ts) | 102 |
14
+ | `@stacksjs/actions` | [`stacks-actions`](../stacks-actions/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/index.ts) | 103 |
15
15
  | `@stacksjs/ai` | [`stacks-ai`](../stacks-ai/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/index.ts) | 21 |
16
16
  | `@stacksjs/alias` | [`stacks-alias`](../stacks-alias/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/alias/src/index.ts) | 3 |
17
17
  | `@stacksjs/analytics` | [`stacks-analytics`](../stacks-analytics/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/analytics/src/index.ts) | 7 |
@@ -71,7 +71,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
71
71
  | `@stacksjs/payments` | [`stacks-payments`](../stacks-payments/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/payments/src/index.ts) | 39 |
72
72
  | `@stacksjs/push` | [`stacks-push`](../stacks-push/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/push/src/index.ts) | 8 |
73
73
  | `@stacksjs/query-builder` | [`stacks-query-builder`](../stacks-query-builder/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/query-builder/src/index.ts) | 7 |
74
- | `@stacksjs/queue` | [`stacks-queue`](../stacks-queue/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/queue/src/index.ts) | 55 |
74
+ | `@stacksjs/queue` | [`stacks-queue`](../stacks-queue/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/queue/src/index.ts) | 56 |
75
75
  | `@stacksjs/realtime` | [`stacks-realtime`](../stacks-realtime/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/realtime/src/index.ts) | 13 |
76
76
  | `@stacksjs/registry` | [`stacks-registry`](../stacks-registry/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/registry/src/index.ts) | 1 |
77
77
  | `@stacksjs/repl` | [`stacks-repl`](../stacks-repl/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/repl/src/index.ts) | 2 |
@@ -119,7 +119,7 @@ Declared runtime exports: `BlogAdminError`, `DEFAULT_IOS_DEPENDENCIES`, `DEFAULT
119
119
 
120
120
  Local source modules: [src/action.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/action.ts), [src/add.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/add.ts), [src/blog-admin.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/blog-admin.ts), [src/build/ios-toolchain.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/build/ios-toolchain.ts), [src/build/macos-native.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/build/macos-native.ts), [src/commit.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/commit.ts), [src/dev/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/dev/index.ts), [src/discover-packages.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/discover-packages.ts), [src/generate/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/generate/index.ts), [src/github-release.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/github-release.ts), [src/helpers/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/helpers/index.ts), [src/library/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/library/index.ts), [src/lint/lint.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/lint/lint.ts), [src/lint/stx-gate.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/lint/stx-gate.ts), [src/make-command.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/make-command.ts), [src/make-job.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/make-job.ts), [src/make-policy.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/make-policy.ts), [src/make-resource.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/make-resource.ts), [src/make.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/make.ts), [src/publication-preflight.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/publication-preflight.ts), [src/release-evidence.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/release-evidence.ts), [src/reviewos-evidence.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/reviewos-evidence.ts), [src/scaffold-crud.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/scaffold-crud.ts), [src/setup/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/setup/index.ts), [src/stacks.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/stacks.ts), [src/stx-source.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/stx-source.ts), [src/upgrade/package-project.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/upgrade/package-project.ts), [src/upgrade/route-actions.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/upgrade/route-actions.ts).
121
121
 
122
- Evidence directory: [102 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
122
+ Evidence directory: [103 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
123
123
 
124
124
  ### @stacksjs/ai
125
125
 
@@ -699,7 +699,7 @@ Declared runtime exports: `Batch`, `DispatchedBatch`, `FailedJobNotifier`, `JOB_
699
699
 
700
700
  Local source modules: [src/action-runner.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/queue/src/action-runner.ts), [src/action.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/queue/src/action.ts), [src/batch.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/queue/src/batch.ts), [src/bun-queue.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/queue/src/bun-queue.ts), [src/circuit-breaker.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/queue/src/circuit-breaker.ts), [src/dead-letter.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/queue/src/dead-letter.ts), [src/discovery.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/queue/src/discovery.ts), [src/driver.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/queue/src/driver.ts), [src/drivers/redis.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/queue/src/drivers/redis.ts), [src/envelope.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/queue/src/envelope.ts), [src/events.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/queue/src/events.ts), [src/health.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/queue/src/health.ts), [src/idempotency.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/queue/src/idempotency.ts), [src/job-progress.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/queue/src/job-progress.ts), [src/job.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/queue/src/job.ts), [src/notifications.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/queue/src/notifications.ts), [src/poison.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/queue/src/poison.ts), [src/scheduler.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/queue/src/scheduler.ts), [src/testing.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/queue/src/testing.ts), [src/worker.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/queue/src/worker.ts).
701
701
 
702
- Evidence directory: [55 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/queue/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
702
+ Evidence directory: [56 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/queue/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
703
703
 
704
704
  ### @stacksjs/realtime
705
705
 
@@ -119,3 +119,15 @@ state for all workers in a fleet.
119
119
  Evidence: `tests/dlq-quarantine-wiring.test.ts`, `failure-path-correctness.test.ts`,
120
120
  `worker-declared-retries.test.ts`, `graceful-shutdown-wiring.test.ts` and
121
121
  `worker-queues.test.ts`. Driver status comes from `core/config/src/capabilities.ts`.
122
+
123
+ ## Graceful database-worker shutdown
124
+
125
+ `stopProcessor` drains database batches from the start of their reservation through
126
+ handler completion. A reservation whose SQL response arrives during shutdown still
127
+ finishes its committed job within the existing grace window. A selection returning
128
+ after shutdown does not start a new reservation. An exhausted grace window still
129
+ falls back to the normal stale-reservation recovery; it does not clear another
130
+ worker's reservation or change attempts or retry deadlines.
131
+
132
+ Evidence: `tests/worker-reservation-drain.test.ts` blocks real SQLite/PostgreSQL
133
+ selection and committed reservation responses while shutdown begins.
@@ -1,97 +1,17 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions/runtime'
2
- import { db } from '@stacksjs/database/runtime'
3
+ import { response } from '@stacksjs/router'
3
4
  import { dashboardOperationalError } from '../dashboard-response'
4
- import type { CommentableRow, DashboardComment } from './comment-input'
5
- import { commentableRecord, isMissingTableError, isSafeTableName, newestFirst, normalizeCommentStatus } from './comment-input'
6
-
7
- interface CommentRow {
8
- id: number
9
- author_name: string | null
10
- author_email: string | null
11
- content: string | null
12
- body: string | null
13
- post_title: string | null
14
- status: string | null
15
- is_approved: number | null
16
- created_at: string | null
17
- updated_at: string | null
18
- }
19
-
20
- /**
21
- * What each trait comment was left on, keyed `type:id`: the owner's title, else
22
- * its name, else `posts #3`. `commentables_type` is the owner's table, so this
23
- * is a join done per type, because one SQL join cannot follow a polymorphic
24
- * key. A type that is not a safe identifier, has no such table, or has neither
25
- * column falls back to the plain label rather than failing the whole list.
26
- */
27
- async function ownerLabels(rows: CommentableRow[]): Promise<Map<string, string>> {
28
- const labels = new Map<string, string>()
29
- const idsByType = new Map<string, number[]>()
30
-
31
- for (const row of rows) {
32
- const type = String(row.commentables_type || '')
33
- labels.set(`${type}:${row.commentables_id}`, `${type} #${row.commentables_id}`)
34
- if (isSafeTableName(type))
35
- idsByType.set(type, [...(idsByType.get(type) ?? []), Number(row.commentables_id)])
36
- }
37
-
38
- for (const [type, ids] of idsByType) {
39
- for (const column of ['title', 'name']) {
40
- try {
41
- const owners = await (db as any)
42
- .selectFrom(type)
43
- .select(['id', column])
44
- .whereIn('id', [...new Set(ids)])
45
- .execute() as Array<Record<string, unknown>>
46
-
47
- for (const owner of owners) {
48
- const label = String(owner[column] ?? '').trim()
49
- if (label)
50
- labels.set(`${type}:${owner.id}`, label)
51
- }
52
- break
53
- }
54
- catch {
55
- // No such table or no such column: try the next column, then keep
56
- // the fallback label.
57
- }
58
- }
59
- }
60
-
61
- return labels
62
- }
63
-
64
- /**
65
- * Comments the `commentable` trait wrote. An app that has never migrated the
66
- * trait tables has none, which is not an error; any other failure is.
67
- */
68
- async function commentableComments(): Promise<DashboardComment[]> {
69
- let rows: CommentableRow[]
70
- try {
71
- rows = await db
72
- .selectFrom('commentables')
73
- .selectAll()
74
- .orderBy('created_at', 'desc')
75
- .execute() as unknown as CommentableRow[]
76
- }
77
- catch (error) {
78
- if (isMissingTableError(error))
79
- return []
80
- throw error
81
- }
82
-
83
- const labels = await ownerLabels(rows)
84
-
85
- return rows.map(row => commentableRecord(row, labels.get(`${row.commentables_type}:${row.commentables_id}`) ?? ''))
86
- }
5
+ import { parseCommentListQuery, readCommentPage } from './comment-list'
87
6
 
88
7
  /**
89
8
  * `GET /api/dashboard/comments` — backs `views/dashboard/content/comments/index.stx`.
90
9
  *
91
- * Lists both places a comment can live, newest first: the model-backed
92
- * `comments` table and the polymorphic `commentables` table the `commentable`
93
- * trait writes. Their ids overlap, so every record carries its `source` and a
94
- * `key` unique across both, and the moderation writes take `source` back.
10
+ * Lists both places a comment can live, newest first, one page at a time:
11
+ * the model-backed `comments` table and the polymorphic `commentables` table
12
+ * the `commentable` trait writes (`comment-list.ts`, stacksjs/stacks#3193).
13
+ * Their ids overlap, so every record carries its `source` and a `key` unique
14
+ * across both, and the moderation writes take `source` back.
95
15
  *
96
16
  * Reads the `comments` table via `db`. The previous `Comment.orderBy(...)` call
97
17
  * threw on every request (the ORM model exposes no query methods) and the catch
@@ -107,31 +27,21 @@ export default new Action({
107
27
  description: 'Returns CMS comments for the dashboard.',
108
28
  method: 'GET',
109
29
  apiResponse: true,
110
- async handle() {
111
- try {
112
- const rows = await db
113
- .selectFrom('comments')
114
- .selectAll()
115
- .orderBy('created_at', 'desc')
116
- .execute() as unknown as CommentRow[]
117
-
118
- const legacy: DashboardComment[] = rows.map(row => ({
119
- key: `comments:${row.id}`,
120
- source: 'comments',
121
- id: Number(row.id),
122
- author_name: String(row.author_name || ''),
123
- author_email: String(row.author_email || ''),
124
- content: String(row.content || row.body || ''),
125
- post_title: String(row.post_title || ''),
126
- status: normalizeCommentStatus(row.status),
127
- is_approved: Boolean(row.is_approved),
128
- created_at: row.created_at || null,
129
- updated_at: row.updated_at || null,
130
- }))
131
-
132
- const comments = [...legacy, ...await commentableComments()].sort(newestFirst)
30
+ async handle(request: RequestInstance) {
31
+ const query = parseCommentListQuery(key => request.get<unknown>(key, ''))
32
+ if ('error' in query)
33
+ return response.json({ message: query.error }, 422)
133
34
 
134
- return { comments }
35
+ try {
36
+ const result = await readCommentPage(query)
37
+
38
+ return {
39
+ comments: result.rows,
40
+ total: result.total,
41
+ page: result.page,
42
+ perPage: query.perPage,
43
+ counts: result.counts,
44
+ }
135
45
  }
136
46
  catch (error) {
137
47
  return dashboardOperationalError(error, 'Comments could not be loaded.', 'CommentIndexAction')
@@ -1,44 +1,36 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions/runtime'
2
- import { db } from '@stacksjs/database/runtime'
3
+ import { response } from '@stacksjs/router'
3
4
  import { dashboardOperationalError } from '../dashboard-response'
4
-
5
- interface PageRow {
6
- id: number
7
- title: string | null
8
- template: string | null
9
- views: number | null
10
- conversions: number | null
11
- published_at: string | null
12
- author_id: number | null
13
- created_at: string | null
14
- updated_at: string | null
15
- }
5
+ import { parsePageListQuery, readPageListPage } from './page-list'
16
6
 
17
7
  /**
18
8
  * `GET /api/dashboard/pages` — backs `views/dashboard/content/pages/index.stx`.
19
9
  *
20
- * Reads the `pages` table via `db`. The previous `Page.orderBy(...)` call threw
21
- * on every request (the ORM model exposes no query methods) and the catch
22
- * turned that into an empty list, so a broken read looked like a CMS with no
10
+ * Reads one page of the `pages` table, searched, sorted and counted by the
11
+ * database (`page-list.ts`, stacksjs/stacks#3192). The previous
12
+ * `Page.orderBy(...)` call threw on every request (the ORM model exposes no
13
+ * query methods) and the catch turned that into an empty list, so a broken read looked like a CMS with no
23
14
  * pages. It also mapped `slug` and `status`, which this table does not have —
24
15
  * a page is a title, a template, and its traffic counters.
25
16
  *
26
- * Ordered newest-first because the page's Newest tile reads the first row.
17
+ * Newest first by default. The Newest tile reads `newestTitle`, which is the
18
+ * newest page whatever the search or sort.
27
19
  */
28
20
  export default new Action({
29
21
  name: 'PageIndexAction',
30
22
  description: 'Returns CMS pages for the dashboard.',
31
23
  method: 'GET',
32
24
  apiResponse: true,
33
- async handle() {
25
+ async handle(request: RequestInstance) {
26
+ const query = parsePageListQuery(key => request.get<unknown>(key, ''))
27
+ if ('error' in query)
28
+ return response.json({ message: query.error }, 422)
29
+
34
30
  try {
35
- const rows = await db
36
- .selectFrom('pages')
37
- .selectAll()
38
- .orderBy('created_at', 'desc')
39
- .execute() as unknown as PageRow[]
31
+ const result = await readPageListPage(query)
40
32
 
41
- const pages = rows.map(row => ({
33
+ const pages = result.rows.map(row => ({
42
34
  id: Number(row.id),
43
35
  title: String(row.title || ''),
44
36
  template: String(row.template || 'default'),
@@ -50,7 +42,16 @@ export default new Action({
50
42
  updated_at: row.updated_at || null,
51
43
  }))
52
44
 
53
- return { pages }
45
+ return {
46
+ pages,
47
+ total: result.total,
48
+ page: result.page,
49
+ perPage: query.perPage,
50
+ allCount: result.allCount,
51
+ totalViews: result.totalViews,
52
+ totalConversions: result.totalConversions,
53
+ newestTitle: result.newestTitle,
54
+ }
54
55
  }
55
56
  catch (error) {
56
57
  return dashboardOperationalError(error, 'Pages could not be loaded.', 'PageIndexAction')
@@ -1,21 +1,9 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions/runtime'
2
3
  import { db } from '@stacksjs/database/runtime'
4
+ import { response } from '@stacksjs/router'
3
5
  import { dashboardOperationalError } from '../dashboard-response'
4
-
5
- interface PostRow {
6
- id: number
7
- title: string
8
- excerpt: string | null
9
- content: string | null
10
- poster: string | null
11
- status: string | null
12
- views: number | null
13
- published_at: string | null
14
- created_at: string | null
15
- updated_at: string | null
16
- author_id: number | null
17
- is_featured: number | null
18
- }
6
+ import { parsePostListQuery, readPostPage } from './post-list'
19
7
 
20
8
  interface PivotRow {
21
9
  relatedId: number
@@ -44,20 +32,14 @@ function normalizeStatus(status: string | null): string {
44
32
  return value === 'published' || value === 'archived' ? value : 'draft'
45
33
  }
46
34
 
47
- function counts(posts: Array<{ status: string }>) {
48
- return {
49
- publishedCount: posts.filter(p => p.status === 'published').length,
50
- draftCount: posts.filter(p => p.status === 'draft').length,
51
- archivedCount: posts.filter(p => p.status === 'archived').length,
52
- }
53
- }
54
-
55
35
  /**
56
36
  * `GET /api/dashboard/posts` — backs `views/dashboard/content/posts/index.stx`.
57
37
  *
58
- * Uses one bounded query per persisted dataset and groups pivot ids in maps,
59
- * avoiding relation N+1 queries. Writes use the same model-declared pivots on
60
- * one native database transaction.
38
+ * Reads one page of posts, searched, filtered, sorted and counted by the
39
+ * database (`post-list.ts`, stacksjs/stacks#3191), and the category and tag
40
+ * links for that page only, grouped in maps to avoid relation N+1 queries.
41
+ * Writes use the same model-declared pivots on one native database
42
+ * transaction.
61
43
  *
62
44
  * There is deliberately no mock-data fallback. This action used to catch every
63
45
  * error and serve placeholder rows, which made a page that was 404ing against
@@ -69,19 +51,19 @@ export default new Action({
69
51
  description: 'Returns CMS posts for the dashboard.',
70
52
  method: 'GET',
71
53
  apiResponse: true,
72
- async handle() {
54
+ async handle(request: RequestInstance) {
55
+ const query = parsePostListQuery(key => request.get<unknown>(key, ''))
56
+ if ('error' in query)
57
+ return response.json({ message: query.error }, 422)
58
+
73
59
  try {
74
- const rows = await db
75
- .selectFrom('posts')
76
- .selectAll()
77
- .orderBy('created_at', 'desc')
78
- .execute() as unknown as PostRow[]
60
+ const page = await readPostPage(query)
79
61
 
80
- const posts = rows.map(row => ({
62
+ const posts = page.rows.map(row => ({
81
63
  id: Number(row.id),
82
64
  title: String(row.title || ''),
83
65
  excerpt: String(row.excerpt || ''),
84
- content: String(row.content || ''),
66
+ preview: String(row.preview || ''),
85
67
  poster: String(row.poster || ''),
86
68
  status: normalizeStatus(row.status),
87
69
  views: Number(row.views || 0),
@@ -121,7 +103,20 @@ export default new Action({
121
103
  tagIds: tagIdsByPost.get(post.id) || [],
122
104
  }))
123
105
 
124
- return { posts: withRelations, categories, tags, authors, ...counts(posts) }
106
+ return {
107
+ posts: withRelations,
108
+ categories,
109
+ tags,
110
+ authors,
111
+ total: page.total,
112
+ page: page.page,
113
+ perPage: query.perPage,
114
+ allCount: page.allCount,
115
+ publishedCount: page.publishedCount,
116
+ draftCount: page.draftCount,
117
+ archivedCount: page.archivedCount,
118
+ totalViews: page.totalViews,
119
+ }
125
120
  }
126
121
  catch (error) {
127
122
  return dashboardOperationalError(error, 'Posts could not be loaded.', 'PostIndexAction')
@@ -0,0 +1,41 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
2
+ import { Action } from '@stacksjs/actions/runtime'
3
+ import { db } from '@stacksjs/database/runtime'
4
+ import { response } from '@stacksjs/router'
5
+ import { dashboardOperationalError } from '../dashboard-response'
6
+
7
+ /**
8
+ * `GET /api/dashboard/posts/{id}` — the body of one post, for the editor.
9
+ *
10
+ * The posts list carries a short preview of each body rather than the whole
11
+ * article (stacksjs/stacks#3191), so the editor reads the body here when it
12
+ * opens. Everything else it shows is already on the list row.
13
+ */
14
+ export default new Action({
15
+ name: 'PostShowAction',
16
+ description: 'Returns the body of one CMS post for the dashboard editor.',
17
+ method: 'GET',
18
+ apiResponse: true,
19
+ async handle(request: RequestInstance) {
20
+ const id = Number(request.getParam('id'))
21
+
22
+ if (!Number.isInteger(id) || id <= 0)
23
+ return response.json({ message: 'A valid post id is required.' }, 422)
24
+
25
+ try {
26
+ const post = await db
27
+ .selectFrom('posts')
28
+ .select(['id', 'content'])
29
+ .where('id', '=', id)
30
+ .executeTakeFirst() as { id: number, content: string | null } | undefined
31
+
32
+ if (!post)
33
+ return response.json({ message: 'Post not found.' }, 404)
34
+
35
+ return { id: Number(post.id), content: String(post.content || '') }
36
+ }
37
+ catch (error) {
38
+ return dashboardOperationalError(error, 'Post could not be loaded.', 'PostShowAction')
39
+ }
40
+ },
41
+ })
@@ -95,6 +95,27 @@ export interface CommentableRow {
95
95
  updated_at?: string | null
96
96
  }
97
97
 
98
+ /** Decode the raw SQL boundary, including integer strings and SQL Date values. */
99
+ export function parseCommentableRow(row: Record<string, unknown>): CommentableRow {
100
+ const text = (value: unknown, field: string): string | null => {
101
+ if (value == null) return null
102
+ if (typeof value !== 'string') throw new TypeError(`Comment ${field} must be text or null`)
103
+ return value
104
+ }
105
+ const id = (value: unknown, field: string): number => {
106
+ const number = typeof value === 'number' || (typeof value === 'string' && /^\d+$/.test(value)) ? Number(value) : Number.NaN
107
+ if (!Number.isSafeInteger(number) || number <= 0) throw new TypeError(`Comment ${field} must be a positive integer`)
108
+ return number
109
+ }
110
+ const timestamp = (value: unknown, field: string): string | null => value instanceof Date ? value.toISOString() : text(value, field)
111
+ return {
112
+ id: id(row.id, 'id'), title: text(row.title, 'title'), body: text(row.body, 'body'), status: text(row.status, 'status'),
113
+ commentables_id: id(row.commentables_id, 'owner id'), commentables_type: text(row.commentables_type, 'owner type') ?? '',
114
+ user_id: row.user_id == null ? null : id(row.user_id, 'user id'), author_name: text(row.author_name, 'author name'), author_email: text(row.author_email, 'author email'),
115
+ created_at: timestamp(row.created_at, 'created at'), updated_at: timestamp(row.updated_at, 'updated at'),
116
+ }
117
+ }
118
+
98
119
  /** One row of the dashboard's comment list, from either table. */
99
120
  export interface DashboardComment {
100
121
  key: string
@@ -152,7 +173,10 @@ export function isSafeTableName(value: string): boolean {
152
173
  * every space-separated one on the same day.
153
174
  */
154
175
  export function newestFirst(a: DashboardComment, b: DashboardComment): number {
155
- const at = (value: string | null) => str(value).replace('T', ' ').slice(0, 19)
176
+ // PostgreSQL and MySQL hand timestamps back as Dates, and `String(date)`
177
+ // starts with the weekday, so it compared Thursday before Wednesday whatever
178
+ // the year (stacksjs/stacks#3193).
179
+ const at = (value: string | Date | null) => (value instanceof Date ? value.toISOString() : str(value)).replace('T', ' ').slice(0, 19)
156
180
 
157
181
  return at(b.created_at).localeCompare(at(a.created_at)) || b.key.localeCompare(a.key)
158
182
  }