@stacksjs/defaults 0.70.356 → 0.70.358
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ai/skills/stacks-browse/SKILL.md +5 -1
- package/ai/skills/stacks-browse/scripts/browse.ts +61 -6
- package/ai/skills/stacks-orm/SKILL.md +6 -0
- package/app/Actions/Dashboard/Actions/GetActions.ts +15 -9
- package/app/Actions/Dashboard/Analytics/EventStoreAction.ts +21 -9
- package/app/Actions/Dashboard/Commerce/ProductUnitDefaultAction.ts +12 -3
- package/app/Actions/Dashboard/Commerce/TaxRateDefaultAction.ts +12 -3
- package/app/Actions/Dashboard/Content/AuthorDestroyAction.ts +23 -9
- package/app/Actions/Dashboard/Content/AuthorIndexAction.ts +32 -26
- package/app/Actions/Dashboard/Content/AuthorStoreAction.ts +44 -24
- package/app/Actions/Dashboard/Content/AuthorUpdateAction.ts +48 -26
- package/app/Actions/Dashboard/Content/CategoryDestroyAction.ts +27 -8
- package/app/Actions/Dashboard/Content/CategoryIndexAction.ts +21 -15
- package/app/Actions/Dashboard/Content/CategoryStoreAction.ts +47 -29
- package/app/Actions/Dashboard/Content/CommentDestroyAction.ts +17 -4
- package/app/Actions/Dashboard/Content/CommentIndexAction.ts +23 -17
- package/app/Actions/Dashboard/Content/CommentUpdateAction.ts +30 -12
- package/app/Actions/Dashboard/Content/ContentDashboardAction.ts +43 -37
- package/app/Actions/Dashboard/Content/PageDestroyAction.ts +17 -4
- package/app/Actions/Dashboard/Content/PageIndexAction.ts +23 -17
- package/app/Actions/Dashboard/Content/PageStoreAction.ts +33 -18
- package/app/Actions/Dashboard/Content/PageUpdateAction.ts +40 -22
- package/app/Actions/Dashboard/Content/PostDestroyAction.ts +24 -11
- package/app/Actions/Dashboard/Content/PostIndexAction.ts +55 -49
- package/app/Actions/Dashboard/Content/PostStoreAction.ts +43 -33
- package/app/Actions/Dashboard/Content/PostUpdateAction.ts +46 -34
- package/app/Actions/Dashboard/Content/TagDestroyAction.ts +27 -8
- package/app/Actions/Dashboard/Content/TagIndexAction.ts +31 -25
- package/app/Actions/Dashboard/Content/TagStoreAction.ts +44 -23
- package/app/Actions/Dashboard/Content/content-input.ts +4 -4
- package/app/Actions/Dashboard/Content/post-input.ts +60 -8
- package/app/Actions/Dashboard/DashboardHealthAction.ts +34 -14
- package/app/Actions/Dashboard/DashboardHomeAction.test.ts +1 -1
- package/app/Actions/Dashboard/DashboardHomeAction.ts +31 -4
- package/app/Actions/Dashboard/DashboardStatsAction.ts +6 -1
- package/app/Actions/Dashboard/Email/InboxSendAction.ts +14 -7
- package/app/Actions/Dashboard/Infrastructure/CommandIndexAction.ts +33 -27
- package/app/Actions/Dashboard/Infrastructure/EnvironmentIndexAction.ts +10 -4
- package/app/Actions/Dashboard/Infrastructure/EnvironmentUpdateAction.ts +8 -1
- package/app/Actions/Dashboard/Infrastructure/RequestIndexAction.ts +24 -18
- package/app/Actions/Dashboard/Infrastructure/ServerIndexAction.ts +15 -9
- package/app/Actions/Dashboard/Infrastructure/ServerShowAction.ts +9 -2
- package/app/Actions/Dashboard/Marketing/CampaignCancelAction.ts +23 -5
- package/app/Actions/Dashboard/Marketing/CampaignDestroyAction.ts +26 -17
- package/app/Actions/Dashboard/Marketing/CampaignIndexAction.ts +49 -43
- package/app/Actions/Dashboard/Marketing/CampaignScheduleAction.ts +31 -25
- package/app/Actions/Dashboard/Marketing/CampaignSendAction.ts +28 -22
- package/app/Actions/Dashboard/Marketing/CampaignStoreAction.ts +15 -10
- package/app/Actions/Dashboard/Marketing/CampaignUpdateAction.ts +19 -12
- package/app/Actions/Dashboard/Marketing/ListDestroyAction.ts +34 -25
- package/app/Actions/Dashboard/Marketing/ListIndexAction.ts +32 -26
- package/app/Actions/Dashboard/Marketing/ListStoreAction.ts +31 -18
- package/app/Actions/Dashboard/Marketing/ListUpdateAction.ts +35 -19
- package/app/Actions/Dashboard/Marketing/SocialPostDestroyAction.ts +15 -5
- package/app/Actions/Dashboard/Marketing/SocialPostIndexAction.ts +11 -5
- package/app/Actions/Dashboard/Marketing/SocialPostStoreAction.ts +19 -13
- package/app/Actions/Dashboard/Marketing/SocialPostUpdateAction.ts +22 -12
- package/app/Actions/Dashboard/Marketing/campaign-delivery.ts +34 -0
- package/app/Actions/Dashboard/Marketing/marketing-list-records.test.ts +10 -0
- package/app/Actions/Dashboard/Marketing/marketing-list-records.ts +10 -0
- package/app/Actions/Dashboard/Marketing/marketing-response.ts +28 -0
- package/app/Actions/Dashboard/Marketing/social-post-records.test.ts +13 -0
- package/app/Actions/Dashboard/Marketing/social-post-records.ts +22 -4
- package/app/Actions/Dashboard/Models/GetModels.ts +14 -7
- package/app/Actions/Dashboard/Models/GetSubscriberCount.ts +8 -1
- package/app/Actions/Dashboard/Models/GetUserCount.ts +8 -1
- package/app/Actions/Dashboard/Releases/ReleaseIndexAction.ts +11 -5
- package/app/Actions/Dashboard/Settings/MailSettingsGetAction.ts +10 -4
- package/app/Actions/Dashboard/Settings/MailSettingsUpdateAction.ts +8 -1
- package/ide/vscode/package.json +1 -1
- package/package.json +1 -1
- package/resources/components/Dashboard/UI/Modal.stx +60 -10
- package/views/dashboard/stores/auth.ts +18 -2
|
@@ -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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
9
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
30
|
+
if (!post)
|
|
31
|
+
return false
|
|
25
32
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
|
59
|
-
*
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
35
|
+
const now = timestamp()
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
55
|
+
const id = insertedId(insertResult)
|
|
51
56
|
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
if (!id)
|
|
58
|
+
throw new Error('Post insert did not return an id.')
|
|
54
59
|
|
|
55
|
-
|
|
60
|
+
await syncPostRelations(trx, id, payload)
|
|
56
61
|
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
post.tags().sync(payload.tagIds),
|
|
63
|
-
])
|
|
68
|
+
if (result.kind === 'invalid')
|
|
69
|
+
return response.json({ message: result.message }, 422)
|
|
64
70
|
|
|
65
|
-
|
|
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
|
})
|
|
@@ -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
|
-
import {
|
|
6
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
7
|
+
import { findPost, invalidPostContent, invalidPostReference, postPayload, publishedAtFor, syncPostRelations, timestamp } from './post-input'
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* `PATCH /api/dashboard/posts/{id}` — updates a CMS post from the dashboard.
|
|
@@ -27,45 +29,55 @@ export default new Action({
|
|
|
27
29
|
if (invalidContent)
|
|
28
30
|
return response.json({ message: invalidContent }, 422)
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
try {
|
|
33
|
+
const result = await transaction(async (rawTrx) => {
|
|
34
|
+
const trx = rawTrx as unknown as typeof db
|
|
35
|
+
const invalidReference = await invalidPostReference(payload, trx)
|
|
36
|
+
if (invalidReference)
|
|
37
|
+
return { kind: 'invalid', message: invalidReference } as const
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
const existing = await trx
|
|
40
|
+
.selectFrom('posts')
|
|
41
|
+
.select(['id', 'published_at'])
|
|
42
|
+
.where('id', '=', id)
|
|
43
|
+
.executeTakeFirst() as { id: number, published_at: string | null } | undefined
|
|
39
44
|
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
if (!existing)
|
|
46
|
+
return { kind: 'not-found' } as const
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
48
|
+
await trx
|
|
49
|
+
.updateTable('posts')
|
|
50
|
+
.set({
|
|
51
|
+
title: payload.title,
|
|
52
|
+
excerpt: payload.excerpt,
|
|
53
|
+
content: payload.content,
|
|
54
|
+
poster: payload.poster,
|
|
55
|
+
status: payload.status,
|
|
56
|
+
author_id: payload.authorId,
|
|
57
|
+
is_featured: payload.featured ? 1 : 0,
|
|
58
|
+
published_at: publishedAtFor(payload.status, existing.published_at, timestamp()),
|
|
59
|
+
updated_at: timestamp(),
|
|
60
|
+
} as any)
|
|
61
|
+
.where('id', '=', id)
|
|
62
|
+
.execute()
|
|
58
63
|
|
|
59
|
-
|
|
64
|
+
await syncPostRelations(trx, id, payload)
|
|
60
65
|
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
const updated = await findPost(id, trx)
|
|
67
|
+
if (!updated)
|
|
68
|
+
throw new Error('Updated post could not be read back.')
|
|
69
|
+
return { kind: 'updated', post: updated } as const
|
|
70
|
+
})
|
|
63
71
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
72
|
+
if (result.kind === 'invalid')
|
|
73
|
+
return response.json({ message: result.message }, 422)
|
|
74
|
+
if (result.kind === 'not-found')
|
|
75
|
+
return response.json({ message: 'Post not found.' }, 404)
|
|
68
76
|
|
|
69
|
-
|
|
77
|
+
return response.json(result.post)
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
return dashboardOperationalError(error, 'Post could not be updated.', 'PostUpdateAction', 500)
|
|
81
|
+
}
|
|
70
82
|
},
|
|
71
83
|
})
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { RequestInstance } from '@stacksjs/types'
|
|
2
2
|
import { Action } from '@stacksjs/actions'
|
|
3
|
+
import { db } from '@stacksjs/database'
|
|
4
|
+
import { transaction } from '@stacksjs/orm'
|
|
3
5
|
import { response } from '@stacksjs/router'
|
|
6
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
4
7
|
import { rowId } from './content-input'
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* `DELETE /api/dashboard/tags/{id}` — deletes a CMS tag.
|
|
8
11
|
*
|
|
9
|
-
* Detaches related posts through the model
|
|
10
|
-
*
|
|
12
|
+
* Detaches related posts through the model-declared pivot before removing the
|
|
13
|
+
* tag, with both writes committed by the same transaction.
|
|
11
14
|
*/
|
|
12
15
|
export default new Action({
|
|
13
16
|
name: 'TagDestroyAction',
|
|
@@ -19,13 +22,29 @@ export default new Action({
|
|
|
19
22
|
if (!id)
|
|
20
23
|
return response.json({ message: 'A valid tag id is required.' }, 422)
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
try {
|
|
26
|
+
const deleted = await transaction(async (rawTrx) => {
|
|
27
|
+
const trx = rawTrx as unknown as typeof db
|
|
28
|
+
const tag = await trx.selectFrom('tags').select(['id']).where('id', '=', id).executeTakeFirst()
|
|
29
|
+
if (!tag)
|
|
30
|
+
return false
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
await trx
|
|
33
|
+
.deleteFrom('taggable_models')
|
|
34
|
+
.where('tag_id', '=', id)
|
|
35
|
+
.where('taggable_type', '=', 'posts')
|
|
36
|
+
.execute()
|
|
37
|
+
await trx.deleteFrom('tags').where('id', '=', id).execute()
|
|
38
|
+
return true
|
|
39
|
+
})
|
|
28
40
|
|
|
29
|
-
|
|
41
|
+
if (!deleted)
|
|
42
|
+
return response.json({ message: 'Tag not found.' }, 404)
|
|
43
|
+
|
|
44
|
+
return response.json({ message: 'Tag deleted.', id })
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
return dashboardOperationalError(error, 'Tag could not be deleted.', 'TagDestroyAction', 500)
|
|
48
|
+
}
|
|
30
49
|
},
|
|
31
50
|
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
2
|
import { db } from '@stacksjs/database'
|
|
3
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
3
4
|
|
|
4
5
|
interface TagRow {
|
|
5
6
|
id: number
|
|
@@ -28,32 +29,37 @@ export default new Action({
|
|
|
28
29
|
method: 'GET',
|
|
29
30
|
apiResponse: true,
|
|
30
31
|
async handle() {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
32
|
+
try {
|
|
33
|
+
const [rows, countRows] = await Promise.all([
|
|
34
|
+
db
|
|
35
|
+
.selectFrom('tags')
|
|
36
|
+
.select(['id', 'name', 'slug', 'description', 'color', 'created_at', 'updated_at'])
|
|
37
|
+
.orderBy('created_at', 'desc')
|
|
38
|
+
.execute() as unknown as Promise<TagRow[]>,
|
|
39
|
+
db
|
|
40
|
+
.selectFrom('taggable_models')
|
|
41
|
+
.select(['tag_id', db.fn.count('taggable_id').as('count')])
|
|
42
|
+
.where('taggable_type', '=', 'posts')
|
|
43
|
+
.groupBy('tag_id')
|
|
44
|
+
.execute() as unknown as Promise<TagCountRow[]>,
|
|
45
|
+
])
|
|
46
|
+
const countByTag = new Map(countRows.map(row => [Number(row.tag_id), Number(row.count)]))
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
const tags = rows.map(row => ({
|
|
49
|
+
id: Number(row.id),
|
|
50
|
+
name: String(row.name || ''),
|
|
51
|
+
slug: String(row.slug || ''),
|
|
52
|
+
description: String(row.description || ''),
|
|
53
|
+
color: String(row.color || ''),
|
|
54
|
+
post_count: countByTag.get(Number(row.id)) || 0,
|
|
55
|
+
created_at: row.created_at || null,
|
|
56
|
+
updated_at: row.updated_at || null,
|
|
57
|
+
}))
|
|
56
58
|
|
|
57
|
-
|
|
59
|
+
return { tags }
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
return dashboardOperationalError(error, 'Tags could not be loaded.', 'TagIndexAction')
|
|
63
|
+
}
|
|
58
64
|
},
|
|
59
65
|
})
|