@stacksjs/defaults 0.70.356 → 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.
- package/ai/skills/stacks-orm/SKILL.md +6 -0
- 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/ide/vscode/package.json +1 -1
- package/package.json +1 -1
|
@@ -196,6 +196,12 @@ await createUser('Alice', 'alice@example.com') // auto-wrapped
|
|
|
196
196
|
|
|
197
197
|
Both `transaction()` and `savepoint()` delegate to `db.transaction()` and `db.savepoint()` from `@stacksjs/database`.
|
|
198
198
|
|
|
199
|
+
### Transaction executor boundary
|
|
200
|
+
|
|
201
|
+
Every query that must commit or roll back together must use the callback handle (`tx` or `sp`), including validation reads, pivot writes, and the final readback. Do not mix `Model.find()`, `Model.create()`, instance `update()` / `delete()`, or instance relation calls into a raw query-builder transaction. The model executor is a separate execution surface and is not rebound to the callback handle. On SQLite it may use a separate connection, so it cannot observe an uncommitted row written through `tx`.
|
|
202
|
+
|
|
203
|
+
`runInTransactionScope()` buffers supported side effects until commit, but it does not rebind model queries. For a transaction-backed custom action, use the model definition as the schema and relationship source of truth, then execute the complete persistence workflow through `tx`. Read the created or updated row through `tx` before returning so a readback failure also rolls back the mutation.
|
|
204
|
+
|
|
199
205
|
## Trait Methods (traits/)
|
|
200
206
|
|
|
201
207
|
### Taggable (when `traits.taggable: true`)
|
|
@@ -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, timestamp } from './content-input'
|
|
6
8
|
|
|
7
9
|
/**
|
|
@@ -22,17 +24,29 @@ export default new Action({
|
|
|
22
24
|
if (!id)
|
|
23
25
|
return response.json({ message: 'A valid author id is required.' }, 422)
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
try {
|
|
28
|
+
const deleted = await transaction(async (rawTrx) => {
|
|
29
|
+
const trx = rawTrx as unknown as typeof db
|
|
30
|
+
if (!await rowExists('authors', id, trx))
|
|
31
|
+
return false
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
await trx
|
|
34
|
+
.updateTable('posts')
|
|
35
|
+
.set({ author_id: null, updated_at: timestamp() } as any)
|
|
36
|
+
.where('author_id', '=', id)
|
|
37
|
+
.execute()
|
|
33
38
|
|
|
34
|
-
|
|
39
|
+
await trx.deleteFrom('authors').where('id', '=', id).execute()
|
|
40
|
+
return true
|
|
41
|
+
})
|
|
35
42
|
|
|
36
|
-
|
|
43
|
+
if (!deleted)
|
|
44
|
+
return response.json({ message: 'Author not found.' }, 404)
|
|
45
|
+
|
|
46
|
+
return response.json({ message: 'Author deleted.', id })
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
return dashboardOperationalError(error, 'Author could not be deleted.', 'AuthorDestroyAction', 500)
|
|
50
|
+
}
|
|
37
51
|
},
|
|
38
52
|
})
|
|
@@ -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 AuthorRow {
|
|
5
6
|
id: number
|
|
@@ -30,36 +31,41 @@ export default new Action({
|
|
|
30
31
|
method: 'GET',
|
|
31
32
|
apiResponse: true,
|
|
32
33
|
async handle() {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
try {
|
|
35
|
+
const [rows, postAuthors] = await Promise.all([
|
|
36
|
+
db.selectFrom('authors').selectAll().orderBy('created_at', 'desc').execute() as unknown as Promise<AuthorRow[]>,
|
|
37
|
+
db.selectFrom('posts').select(['author_id']).execute() as unknown as Promise<Array<{ author_id: number | null }>>,
|
|
38
|
+
])
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const postCounts = new Map<number, number>()
|
|
41
|
+
for (const post of postAuthors) {
|
|
42
|
+
if (post.author_id == null)
|
|
43
|
+
continue
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
const id = Number(post.author_id)
|
|
46
|
+
postCounts.set(id, (postCounts.get(id) || 0) + 1)
|
|
47
|
+
}
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
const authors = rows.map(row => ({
|
|
50
|
+
id: Number(row.id),
|
|
51
|
+
name: String(row.name || ''),
|
|
52
|
+
email: String(row.email || ''),
|
|
53
|
+
bio: String(row.bio || ''),
|
|
54
|
+
avatar: String(row.avatar || ''),
|
|
55
|
+
postCount: postCounts.get(Number(row.id)) || 0,
|
|
56
|
+
created_at: row.created_at || null,
|
|
57
|
+
updated_at: row.updated_at || null,
|
|
58
|
+
}))
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
// Posts whose author no longer exists are not counted: the stat means
|
|
61
|
+
// "posts reachable from this list", which is what the page's Assigned Posts
|
|
62
|
+
// tile claims to show.
|
|
63
|
+
const assignedPostCount = authors.reduce((sum, author) => sum + author.postCount, 0)
|
|
62
64
|
|
|
63
|
-
|
|
65
|
+
return { authors, assignedPostCount }
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
return dashboardOperationalError(error, 'Authors could not be loaded.', 'AuthorIndexAction')
|
|
69
|
+
}
|
|
64
70
|
},
|
|
65
71
|
})
|
|
@@ -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 { isUniqueViolation, 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 { parseAuthorInput } from './author-input'
|
|
7
9
|
import { findRow, insertedId, timestamp } from './content-input'
|
|
8
10
|
|
|
@@ -25,35 +27,53 @@ export default new Action({
|
|
|
25
27
|
if ('message' in input)
|
|
26
28
|
return response.json({ message: input.message }, 422)
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
try {
|
|
31
|
+
const author = await transaction(async (rawTrx) => {
|
|
32
|
+
const trx = rawTrx as unknown as typeof db
|
|
33
|
+
const duplicate = await trx
|
|
34
|
+
.selectFrom('authors')
|
|
35
|
+
.select(['id'])
|
|
36
|
+
.where('email', '=', input.data.email)
|
|
37
|
+
.executeTakeFirst()
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
if (duplicate)
|
|
40
|
+
return null
|
|
36
41
|
|
|
37
|
-
|
|
42
|
+
const now = timestamp()
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
const result = await trx
|
|
45
|
+
.insertInto('authors')
|
|
46
|
+
.values({
|
|
47
|
+
uuid: randomUUIDv7(),
|
|
48
|
+
name: input.data.name,
|
|
49
|
+
email: input.data.email,
|
|
50
|
+
bio: input.data.bio || null,
|
|
51
|
+
avatar: input.data.avatar || null,
|
|
52
|
+
created_at: now,
|
|
53
|
+
updated_at: now,
|
|
54
|
+
} as any)
|
|
55
|
+
.executeTakeFirst()
|
|
51
56
|
|
|
52
|
-
|
|
57
|
+
const id = insertedId(result)
|
|
53
58
|
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
if (!id)
|
|
60
|
+
throw new Error('Author insert did not return an id.')
|
|
56
61
|
|
|
57
|
-
|
|
62
|
+
const created = await findRow('authors', id, trx)
|
|
63
|
+
if (!created)
|
|
64
|
+
throw new Error('Created author could not be loaded.')
|
|
65
|
+
return created
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
if (!author)
|
|
69
|
+
return response.json({ message: 'An author with that email already exists.' }, 422)
|
|
70
|
+
|
|
71
|
+
return response.json(author, 201)
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (isUniqueViolation(error))
|
|
75
|
+
return response.json({ message: 'An author with that email already exists.' }, 422)
|
|
76
|
+
return dashboardOperationalError(error, 'Author could not be created.', 'AuthorStoreAction', 500)
|
|
77
|
+
}
|
|
58
78
|
},
|
|
59
79
|
})
|
|
@@ -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 { isUniqueViolation, transaction } from '@stacksjs/orm'
|
|
4
5
|
import { response } from '@stacksjs/router'
|
|
6
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
5
7
|
import { parseAuthorInput } from './author-input'
|
|
6
8
|
import { findRow, rowExists, rowId, timestamp } from './content-input'
|
|
7
9
|
|
|
@@ -15,36 +17,56 @@ export default new Action({
|
|
|
15
17
|
if (!id)
|
|
16
18
|
return response.json({ message: 'A valid author id is required.' }, 422)
|
|
17
19
|
|
|
18
|
-
if (!await rowExists('authors', id))
|
|
19
|
-
return response.json({ message: 'Author not found.' }, 404)
|
|
20
|
-
|
|
21
20
|
const input = parseAuthorInput(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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
25
|
+
try {
|
|
26
|
+
const result = await transaction(async (rawTrx) => {
|
|
27
|
+
const trx = rawTrx as unknown as typeof db
|
|
28
|
+
if (!await rowExists('authors', id, trx))
|
|
29
|
+
return { kind: 'not-found' } as const
|
|
30
|
+
|
|
31
|
+
const duplicate = await trx
|
|
32
|
+
.selectFrom('authors')
|
|
33
|
+
.select(['id'])
|
|
34
|
+
.where('email', '=', input.data.email)
|
|
35
|
+
.where('id', '!=', id)
|
|
36
|
+
.executeTakeFirst()
|
|
37
|
+
|
|
38
|
+
if (duplicate)
|
|
39
|
+
return { kind: 'duplicate' } as const
|
|
40
|
+
|
|
41
|
+
await trx
|
|
42
|
+
.updateTable('authors')
|
|
43
|
+
.set({
|
|
44
|
+
name: input.data.name,
|
|
45
|
+
email: input.data.email,
|
|
46
|
+
bio: input.data.bio || null,
|
|
47
|
+
avatar: input.data.avatar || null,
|
|
48
|
+
updated_at: timestamp(),
|
|
49
|
+
} as any)
|
|
50
|
+
.where('id', '=', id)
|
|
51
|
+
.execute()
|
|
52
|
+
|
|
53
|
+
const author = await findRow('authors', id, trx)
|
|
54
|
+
if (!author)
|
|
55
|
+
throw new Error('Updated author could not be loaded.')
|
|
56
|
+
return { kind: 'updated', author } as const
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
if (result.kind === 'not-found')
|
|
60
|
+
return response.json({ message: 'Author not found.' }, 404)
|
|
61
|
+
if (result.kind === 'duplicate')
|
|
62
|
+
return response.json({ message: 'An author with that email already exists.' }, 422)
|
|
63
|
+
|
|
64
|
+
return response.json(result.author)
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (isUniqueViolation(error))
|
|
68
|
+
return response.json({ message: 'An author with that email already exists.' }, 422)
|
|
69
|
+
return dashboardOperationalError(error, 'Author could not be updated.', 'AuthorUpdateAction', 500)
|
|
70
|
+
}
|
|
49
71
|
},
|
|
50
72
|
})
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { RequestInstance } from '@stacksjs/types'
|
|
2
2
|
import { Action } from '@stacksjs/actions'
|
|
3
|
+
import { db } from '@stacksjs/database'
|
|
4
|
+
import { transaction } from '@stacksjs/orm'
|
|
3
5
|
import { response } from '@stacksjs/router'
|
|
6
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
4
7
|
import { rowId } from './content-input'
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* `DELETE /api/dashboard/categories/{id}` — deletes a CMS category.
|
|
8
11
|
*
|
|
9
|
-
* Detaches related posts through the model
|
|
10
|
-
* category
|
|
12
|
+
* Detaches related posts through the model-declared pivot before removing the
|
|
13
|
+
* category, with both writes committed by the same transaction.
|
|
11
14
|
*/
|
|
12
15
|
export default new Action({
|
|
13
16
|
name: 'CategoryDestroyAction',
|
|
@@ -19,13 +22,29 @@ export default new Action({
|
|
|
19
22
|
if (!id)
|
|
20
23
|
return response.json({ message: 'A valid category id is required.' }, 422)
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
try {
|
|
26
|
+
const deleted = await transaction(async (rawTrx) => {
|
|
27
|
+
const trx = rawTrx as unknown as typeof db
|
|
28
|
+
const category = await trx.selectFrom('categories').select(['id']).where('id', '=', id).executeTakeFirst()
|
|
29
|
+
if (!category)
|
|
30
|
+
return false
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
await trx
|
|
33
|
+
.deleteFrom('categorizable_models')
|
|
34
|
+
.where('category_id', '=', id)
|
|
35
|
+
.where('categorizable_type', '=', 'posts')
|
|
36
|
+
.execute()
|
|
37
|
+
await trx.deleteFrom('categories').where('id', '=', id).execute()
|
|
38
|
+
return true
|
|
39
|
+
})
|
|
28
40
|
|
|
29
|
-
|
|
41
|
+
if (!deleted)
|
|
42
|
+
return response.json({ message: 'Category not found.' }, 404)
|
|
43
|
+
|
|
44
|
+
return response.json({ message: 'Category deleted.', id })
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
return dashboardOperationalError(error, 'Category could not be deleted.', 'CategoryDestroyAction', 500)
|
|
48
|
+
}
|
|
30
49
|
},
|
|
31
50
|
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
2
|
import { db } from '@stacksjs/database'
|
|
3
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
3
4
|
|
|
4
5
|
interface CategoryRow {
|
|
5
6
|
id: number
|
|
@@ -29,22 +30,27 @@ export default new Action({
|
|
|
29
30
|
method: 'GET',
|
|
30
31
|
apiResponse: true,
|
|
31
32
|
async handle() {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
try {
|
|
34
|
+
const rows = await db
|
|
35
|
+
.selectFrom('categories')
|
|
36
|
+
.selectAll()
|
|
37
|
+
.orderBy('created_at', 'desc')
|
|
38
|
+
.execute() as unknown as CategoryRow[]
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
const categories = rows.map(row => ({
|
|
41
|
+
id: Number(row.id),
|
|
42
|
+
name: String(row.name || ''),
|
|
43
|
+
slug: String(row.slug || ''),
|
|
44
|
+
description: String(row.description || ''),
|
|
45
|
+
is_active: row.is_active == null ? true : Boolean(row.is_active),
|
|
46
|
+
created_at: row.created_at || null,
|
|
47
|
+
updated_at: row.updated_at || null,
|
|
48
|
+
}))
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
return { categories }
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
return dashboardOperationalError(error, 'Categories could not be loaded.', 'CategoryIndexAction')
|
|
54
|
+
}
|
|
49
55
|
},
|
|
50
56
|
})
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { RequestInstance } from '@stacksjs/types'
|
|
2
2
|
import { Action } from '@stacksjs/actions'
|
|
3
3
|
import { db } from '@stacksjs/database'
|
|
4
|
+
import { transaction } from '@stacksjs/orm'
|
|
4
5
|
import { response } from '@stacksjs/router'
|
|
5
6
|
import { randomUUIDv7 } from 'bun'
|
|
7
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
6
8
|
import { findRow, insertedId, slugify, str, timestamp } from './content-input'
|
|
7
9
|
|
|
8
10
|
/**
|
|
@@ -26,40 +28,56 @@ export default new Action({
|
|
|
26
28
|
if (!slug)
|
|
27
29
|
return response.json({ message: 'Slug could not be derived from the name; enter one.' }, 422)
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
try {
|
|
32
|
+
const category = await transaction(async (rawTrx) => {
|
|
33
|
+
const trx = rawTrx as unknown as typeof db
|
|
34
|
+
const duplicate = await trx
|
|
35
|
+
.selectFrom('categories')
|
|
36
|
+
.select(['id'])
|
|
37
|
+
.where('slug', '=', slug)
|
|
38
|
+
.executeTakeFirst()
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
if (duplicate)
|
|
41
|
+
return null
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
const now = timestamp()
|
|
44
|
+
const displayOrder = await trx
|
|
45
|
+
.selectFrom('categories')
|
|
46
|
+
.select(db.fn.max('display_order').as('max_display_order'))
|
|
47
|
+
.executeTakeFirst()
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
const result = await trx
|
|
50
|
+
.insertInto('categories')
|
|
51
|
+
.values({
|
|
52
|
+
uuid: randomUUIDv7(),
|
|
53
|
+
name,
|
|
54
|
+
slug,
|
|
55
|
+
description,
|
|
56
|
+
is_active: 1,
|
|
57
|
+
display_order: Number(displayOrder?.max_display_order || 0) + 1,
|
|
58
|
+
created_at: now,
|
|
59
|
+
updated_at: now,
|
|
60
|
+
} as any)
|
|
61
|
+
.executeTakeFirst()
|
|
57
62
|
|
|
58
|
-
|
|
63
|
+
const id = insertedId(result)
|
|
59
64
|
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
if (!id)
|
|
66
|
+
throw new Error('Category insert did not return an id.')
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
const created = await findRow('categories', id, trx)
|
|
69
|
+
if (!created)
|
|
70
|
+
throw new Error('Created category could not be loaded.')
|
|
71
|
+
return created
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
if (!category)
|
|
75
|
+
return response.json({ message: 'A category with that slug already exists.' }, 422)
|
|
76
|
+
|
|
77
|
+
return response.json(category, 201)
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
return dashboardOperationalError(error, 'Category could not be created.', 'CategoryStoreAction', 500)
|
|
81
|
+
}
|
|
64
82
|
},
|
|
65
83
|
})
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { RequestInstance } from '@stacksjs/types'
|
|
2
2
|
import { Action } from '@stacksjs/actions'
|
|
3
3
|
import { db } from '@stacksjs/database'
|
|
4
|
+
import { transaction } from '@stacksjs/orm'
|
|
4
5
|
import { response } from '@stacksjs/router'
|
|
6
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
5
7
|
import { rowExists, rowId } from './content-input'
|
|
6
8
|
|
|
7
9
|
/**
|
|
@@ -20,11 +22,22 @@ export default new Action({
|
|
|
20
22
|
if (!id)
|
|
21
23
|
return response.json({ message: 'A valid comment id is required.' }, 422)
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
try {
|
|
26
|
+
const deleted = await transaction(async (rawTrx) => {
|
|
27
|
+
const trx = rawTrx as unknown as typeof db
|
|
28
|
+
if (!await rowExists('comments', id, trx))
|
|
29
|
+
return false
|
|
30
|
+
await trx.deleteFrom('comments').where('id', '=', id).execute()
|
|
31
|
+
return true
|
|
32
|
+
})
|
|
25
33
|
|
|
26
|
-
|
|
34
|
+
if (!deleted)
|
|
35
|
+
return response.json({ message: 'Comment not found.' }, 404)
|
|
27
36
|
|
|
28
|
-
|
|
37
|
+
return response.json({ message: 'Comment deleted.', id })
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
return dashboardOperationalError(error, 'Comment could not be deleted.', 'CommentDestroyAction', 500)
|
|
41
|
+
}
|
|
29
42
|
},
|
|
30
43
|
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Action } from '@stacksjs/actions'
|
|
2
2
|
import { db } from '@stacksjs/database'
|
|
3
|
+
import { dashboardOperationalError } from '../dashboard-response'
|
|
3
4
|
import { normalizeCommentStatus } from './comment-input'
|
|
4
5
|
|
|
5
6
|
interface CommentRow {
|
|
@@ -33,24 +34,29 @@ export default new Action({
|
|
|
33
34
|
method: 'GET',
|
|
34
35
|
apiResponse: true,
|
|
35
36
|
async handle() {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
try {
|
|
38
|
+
const rows = await db
|
|
39
|
+
.selectFrom('comments')
|
|
40
|
+
.selectAll()
|
|
41
|
+
.orderBy('created_at', 'desc')
|
|
42
|
+
.execute() as unknown as CommentRow[]
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
const comments = rows.map(row => ({
|
|
45
|
+
id: Number(row.id),
|
|
46
|
+
author_name: String(row.author_name || ''),
|
|
47
|
+
author_email: String(row.author_email || ''),
|
|
48
|
+
content: String(row.content || row.body || ''),
|
|
49
|
+
post_title: String(row.post_title || ''),
|
|
50
|
+
status: normalizeCommentStatus(row.status),
|
|
51
|
+
is_approved: Boolean(row.is_approved),
|
|
52
|
+
created_at: row.created_at || null,
|
|
53
|
+
updated_at: row.updated_at || null,
|
|
54
|
+
}))
|
|
53
55
|
|
|
54
|
-
|
|
56
|
+
return { comments }
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
return dashboardOperationalError(error, 'Comments could not be loaded.', 'CommentIndexAction')
|
|
60
|
+
}
|
|
55
61
|
},
|
|
56
62
|
})
|