@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.
- package/ai/skills/stacks-actions/SKILL.md +1 -1
- package/ai/skills/stacks-native/CATALOG.md +4 -4
- package/ai/skills/stacks-queue/DURABLE-WORKFLOWS.md +12 -0
- package/app/Actions/Dashboard/Content/CommentIndexAction.ts +22 -112
- package/app/Actions/Dashboard/Content/PageIndexAction.ts +26 -25
- package/app/Actions/Dashboard/Content/PostIndexAction.ts +30 -35
- package/app/Actions/Dashboard/Content/PostShowAction.ts +41 -0
- package/app/Actions/Dashboard/Content/comment-input.ts +25 -1
- package/app/Actions/Dashboard/Content/comment-list.ts +311 -0
- package/app/Actions/Dashboard/Content/content-list.ts +69 -0
- package/app/Actions/Dashboard/Content/page-list.ts +108 -0
- package/app/Actions/Dashboard/Content/post-list.ts +140 -0
- package/app/Actions/Dashboard/Jobs/JobIndexAction.ts +12 -24
- package/app/Actions/Dashboard/Jobs/job-list.ts +168 -0
- package/app/Actions/Dashboard/Jobs/job-records.ts +8 -3
- package/app/Actions/Dashboard/Models/ModelShowAction.ts +8 -2
- package/app/Actions/Dashboard/Queries/QueryDashboardAction.ts +20 -10
- package/app/Actions/Dashboard/Queries/query-dashboard.ts +56 -9
- package/app/Actions/Dashboard/Queries/query-list.ts +161 -0
- package/app/Actions/Dashboard/Queue/QueueStatsAction.ts +14 -52
- package/app/Actions/Dashboard/Queue/queue-stats.ts +69 -0
- package/app/Actions/Monitoring/ErrorIndexAction.ts +9 -4
- package/app/Actions/Monitoring/error-provider.ts +85 -7
- package/functions/api.ts +14 -10
- package/functions/commerce/coupons.ts +4 -6
- package/functions/commerce/customers.ts +4 -6
- package/functions/commerce/gift-cards.ts +4 -2
- package/functions/commerce/orders.ts +4 -2
- package/functions/commerce/payments.ts +4 -2
- package/functions/commerce/products/categories.ts +4 -6
- package/functions/commerce/products/manufacturers.ts +4 -6
- package/functions/commerce/products/products.ts +4 -6
- package/functions/commerce/products/reviews.ts +4 -6
- package/functions/commerce/products/units.ts +4 -6
- package/functions/commerce/products/variants.ts +4 -6
- package/functions/commerce/taxes.ts +4 -2
- package/functions/commerce/waitlist/products.ts +4 -6
- package/functions/commerce/waitlist/restaurants.ts +4 -6
- package/functions/monitoring/errors.ts +77 -49
- package/functions/queries.ts +34 -2
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/resources/components/Dashboard/Analytics/AnalyticsHub.stx +4 -0
- package/resources/components/Dashboard/Auth/MagicLink.stx +6 -1
- package/resources/components/Dashboard/Content/CommentsDashboard.stx +90 -12
- package/resources/components/Dashboard/Content/PagesDashboard.stx +100 -16
- package/resources/components/Dashboard/Content/PostEditorDialog.stx +28 -8
- package/resources/components/Dashboard/Content/PostsDashboard.stx +179 -21
- package/resources/components/Dashboard/Monitoring/ErrorDashboard.stx +121 -36
- package/resources/components/Dashboard/Monitoring/ErrorTable.stx +29 -8
- package/resources/components/Dashboard/Queries/QueryDashboard.stx +73 -64
- package/resources/components/Dashboard/UI/Table.stx +30 -7
- package/routes/dashboard-api.ts +1 -0
- 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
|
|
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) |
|
|
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) |
|
|
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: [
|
|
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: [
|
|
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 {
|
|
3
|
+
import { response } from '@stacksjs/router'
|
|
3
4
|
import { dashboardOperationalError } from '../dashboard-response'
|
|
4
|
-
import
|
|
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
|
|
92
|
-
* `comments` table and the polymorphic `commentables` table
|
|
93
|
-
* trait writes
|
|
94
|
-
*
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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 {
|
|
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
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
}
|