@stacksjs/cms 0.70.53 → 0.70.55
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/package.json +3 -2
- package/src/authors/destroy.ts +55 -0
- package/src/authors/fetch.ts +70 -0
- package/src/authors/index.ts +21 -0
- package/src/authors/store.ts +150 -0
- package/src/authors/update.ts +42 -0
- package/src/build.ts +1382 -0
- package/src/categorizables/destroy.ts +64 -0
- package/src/categorizables/fetch.ts +121 -0
- package/src/categorizables/index.ts +22 -0
- package/src/categorizables/store.ts +140 -0
- package/src/categorizables/update.ts +57 -0
- package/src/commentables/destroy.ts +64 -0
- package/src/commentables/fetch.ts +319 -0
- package/src/commentables/index.ts +25 -0
- package/src/commentables/store.ts +156 -0
- package/src/commentables/update.ts +75 -0
- package/src/database.ts +9 -0
- package/src/index.ts +42 -0
- package/src/pages/destroy.ts +64 -0
- package/src/pages/fetch.ts +82 -0
- package/src/pages/index.ts +21 -0
- package/src/pages/store.ts +42 -0
- package/src/pages/update.ts +39 -0
- package/src/posts/destroy.ts +90 -0
- package/src/posts/fetch.ts +82 -0
- package/src/posts/index.ts +25 -0
- package/src/posts/store.ts +213 -0
- package/src/posts/update.ts +46 -0
- package/src/taggables/destroy.ts +49 -0
- package/src/taggables/fetch.ts +282 -0
- package/src/taggables/index.ts +17 -0
- package/src/taggables/store.ts +122 -0
- package/src/taggables/update.ts +74 -0
- package/src/tests/categorizables.test.ts +66 -0
- package/src/tests/setup.ts +120 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
type PageJsonResponse = ModelRow<typeof Page>
|
|
2
|
+
import { getDb } from '../database'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fetch a page by ID
|
|
6
|
+
*/
|
|
7
|
+
export async function fetchById(id: number): Promise<PageJsonResponse | undefined> {
|
|
8
|
+
const db = await getDb()
|
|
9
|
+
return await db
|
|
10
|
+
.selectFrom('pages')
|
|
11
|
+
.where('id', '=', id)
|
|
12
|
+
.selectAll()
|
|
13
|
+
.executeTakeFirst() as PageJsonResponse | undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Fetch all pages
|
|
18
|
+
*/
|
|
19
|
+
export async function fetchAll(): Promise<PageJsonResponse[]> {
|
|
20
|
+
const db = await getDb()
|
|
21
|
+
return await db.selectFrom('pages').selectAll().execute() as PageJsonResponse[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Fetch pages by template
|
|
26
|
+
*/
|
|
27
|
+
export async function fetchByTemplate(template: string): Promise<PageJsonResponse[]> {
|
|
28
|
+
const db = await getDb()
|
|
29
|
+
return await db
|
|
30
|
+
.selectFrom('pages')
|
|
31
|
+
.where('template', '=', template)
|
|
32
|
+
.selectAll()
|
|
33
|
+
.execute() as PageJsonResponse[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Fetch pages by author
|
|
38
|
+
*/
|
|
39
|
+
export async function fetchByAuthor(authorId: number): Promise<PageJsonResponse[]> {
|
|
40
|
+
const db = await getDb()
|
|
41
|
+
return await db
|
|
42
|
+
.selectFrom('pages')
|
|
43
|
+
.where('author_id', '=', authorId)
|
|
44
|
+
.selectAll()
|
|
45
|
+
.execute() as PageJsonResponse[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Fetch pages with minimum views
|
|
50
|
+
*/
|
|
51
|
+
export async function fetchByMinViews(minViews: number): Promise<PageJsonResponse[]> {
|
|
52
|
+
const db = await getDb()
|
|
53
|
+
return await db
|
|
54
|
+
.selectFrom('pages')
|
|
55
|
+
.where('views', '>=', minViews)
|
|
56
|
+
.selectAll()
|
|
57
|
+
.execute() as PageJsonResponse[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Fetch pages with minimum conversions
|
|
62
|
+
*/
|
|
63
|
+
export async function fetchByMinConversions(minConversions: number): Promise<PageJsonResponse[]> {
|
|
64
|
+
const db = await getDb()
|
|
65
|
+
return await db
|
|
66
|
+
.selectFrom('pages')
|
|
67
|
+
.where('conversions', '>=', minConversions)
|
|
68
|
+
.selectAll()
|
|
69
|
+
.execute() as PageJsonResponse[]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Fetch pages published after a specific date
|
|
74
|
+
*/
|
|
75
|
+
export async function fetchPublishedAfter(timestamp: number): Promise<PageJsonResponse[]> {
|
|
76
|
+
const db = await getDb()
|
|
77
|
+
return await db
|
|
78
|
+
.selectFrom('pages')
|
|
79
|
+
.where('published_at', '>', timestamp)
|
|
80
|
+
.selectAll()
|
|
81
|
+
.execute() as PageJsonResponse[]
|
|
82
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export {
|
|
2
|
+
bulkDestroy,
|
|
3
|
+
destroy,
|
|
4
|
+
} from './destroy'
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
fetchAll,
|
|
8
|
+
fetchByAuthor,
|
|
9
|
+
fetchById,
|
|
10
|
+
fetchByMinConversions,
|
|
11
|
+
fetchByMinViews,
|
|
12
|
+
fetchPublishedAfter,
|
|
13
|
+
} from './fetch'
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
store,
|
|
17
|
+
} from './store'
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
update,
|
|
21
|
+
} from './update'
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
type PageJsonResponse = ModelRow<typeof Page>
|
|
2
|
+
type NewPage = NewModelData<typeof Page>
|
|
3
|
+
import { getDb } from '../database'
|
|
4
|
+
import { formatDate } from '@stacksjs/orm'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a new page
|
|
8
|
+
*
|
|
9
|
+
* @param data The page data to create
|
|
10
|
+
* @returns The created page record
|
|
11
|
+
*/
|
|
12
|
+
export async function store(data: NewPage): Promise<PageJsonResponse> {
|
|
13
|
+
const db = await getDb()
|
|
14
|
+
try {
|
|
15
|
+
const pageData = {
|
|
16
|
+
author_id: data.author_id,
|
|
17
|
+
title: data.title,
|
|
18
|
+
template: data.template,
|
|
19
|
+
views: data.views || 0,
|
|
20
|
+
conversions: data.conversions || 0,
|
|
21
|
+
created_at: formatDate(new Date()),
|
|
22
|
+
updated_at: formatDate(new Date()),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const result = await db
|
|
26
|
+
.insertInto('pages')
|
|
27
|
+
.values(pageData)
|
|
28
|
+
.returningAll()
|
|
29
|
+
.executeTakeFirst()
|
|
30
|
+
|
|
31
|
+
if (!result)
|
|
32
|
+
throw new Error('Failed to create page')
|
|
33
|
+
|
|
34
|
+
return result as PageJsonResponse
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error instanceof Error)
|
|
38
|
+
throw new TypeError(`Failed to create page: ${error.message}`)
|
|
39
|
+
|
|
40
|
+
throw error
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
type PageJsonResponse = ModelRow<typeof Page>
|
|
2
|
+
type PageUpdate = UpdateModelData<typeof Page>
|
|
3
|
+
import { getDb } from '../database'
|
|
4
|
+
import { formatDate } from '@stacksjs/orm'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Update a page
|
|
8
|
+
*
|
|
9
|
+
* @param id The id of the page to update
|
|
10
|
+
* @param data The page data to update
|
|
11
|
+
* @returns The updated page record
|
|
12
|
+
*/
|
|
13
|
+
export async function update(id: number, data: Partial<PageUpdate>): Promise<PageJsonResponse> {
|
|
14
|
+
const db = await getDb()
|
|
15
|
+
try {
|
|
16
|
+
const updateData = {
|
|
17
|
+
...data,
|
|
18
|
+
updated_at: formatDate(new Date()),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const result = await db
|
|
22
|
+
.updateTable('pages')
|
|
23
|
+
.set(updateData)
|
|
24
|
+
.where('id', '=', id)
|
|
25
|
+
.returningAll()
|
|
26
|
+
.executeTakeFirst()
|
|
27
|
+
|
|
28
|
+
if (!result)
|
|
29
|
+
throw new Error('Failed to update page')
|
|
30
|
+
|
|
31
|
+
return result as PageJsonResponse
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error instanceof Error)
|
|
35
|
+
throw new TypeError(`Failed to update page: ${error.message}`)
|
|
36
|
+
|
|
37
|
+
throw error
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { getDb } from '../database'
|
|
2
|
+
import { fetchById } from './fetch'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Delete a post by ID
|
|
6
|
+
*
|
|
7
|
+
* @param id The ID of the post to delete
|
|
8
|
+
* @returns True if the deletion was successful, false otherwise
|
|
9
|
+
*/
|
|
10
|
+
export async function destroy(id: number): Promise<boolean> {
|
|
11
|
+
const db = await getDb()
|
|
12
|
+
try {
|
|
13
|
+
// First check if the post exists
|
|
14
|
+
const post = await fetchById(id)
|
|
15
|
+
|
|
16
|
+
if (!post) {
|
|
17
|
+
throw new Error(`Post with ID ${id} not found`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Delete related categorizable_models entries first
|
|
21
|
+
await db
|
|
22
|
+
.deleteFrom('categorizable_models')
|
|
23
|
+
.where('categorizable_id', '=', id)
|
|
24
|
+
.where('categorizable_type', '=', 'posts')
|
|
25
|
+
.execute()
|
|
26
|
+
|
|
27
|
+
// Delete the post
|
|
28
|
+
const result = await db
|
|
29
|
+
.deleteFrom('posts')
|
|
30
|
+
.where('id', '=', id)
|
|
31
|
+
.executeTakeFirst()
|
|
32
|
+
|
|
33
|
+
return (result as { numDeletedRows?: number })?.numDeletedRows ? (result as { numDeletedRows: number }).numDeletedRows > 0 : false
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (error instanceof Error) {
|
|
37
|
+
throw new TypeError(`Failed to delete post: ${error.message}`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
throw error
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Bulk delete multiple posts
|
|
46
|
+
*
|
|
47
|
+
* @param ids Array of post IDs to delete
|
|
48
|
+
* @returns Number of posts successfully deleted
|
|
49
|
+
*/
|
|
50
|
+
export async function bulkDestroy(ids: number[]): Promise<number> {
|
|
51
|
+
const db = await getDb()
|
|
52
|
+
if (!ids.length)
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// First verify which posts actually exist
|
|
57
|
+
const existingPosts = await db
|
|
58
|
+
.selectFrom('posts')
|
|
59
|
+
.select(['id'])
|
|
60
|
+
.where('id', 'in', ids)
|
|
61
|
+
.execute()
|
|
62
|
+
|
|
63
|
+
const existingIds = existingPosts.map((post: Record<string, unknown>) => post.id as number)
|
|
64
|
+
|
|
65
|
+
if (!existingIds.length)
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
// Delete related categorizable_models entries first
|
|
69
|
+
await db
|
|
70
|
+
.deleteFrom('categorizable_models')
|
|
71
|
+
.where('categorizable_id', 'in', existingIds)
|
|
72
|
+
.where('categorizable_type', '=', 'posts')
|
|
73
|
+
.execute()
|
|
74
|
+
|
|
75
|
+
// Delete all existing posts
|
|
76
|
+
const result = await db
|
|
77
|
+
.deleteFrom('posts')
|
|
78
|
+
.where('id', 'in', existingIds)
|
|
79
|
+
.executeTakeFirst()
|
|
80
|
+
|
|
81
|
+
return Number((result as { numDeletedRows?: number })?.numDeletedRows) || 0
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error instanceof Error) {
|
|
85
|
+
throw new TypeError(`Failed to delete posts in bulk: ${error.message}`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
throw error
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
type PostJsonResponse = ModelRow<typeof Post>
|
|
2
|
+
import { getDb } from '../database'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fetch a post by ID
|
|
6
|
+
*/
|
|
7
|
+
export async function fetchById(id: number): Promise<PostJsonResponse | undefined> {
|
|
8
|
+
const db = await getDb()
|
|
9
|
+
return await db
|
|
10
|
+
.selectFrom('posts')
|
|
11
|
+
.where('id', '=', id)
|
|
12
|
+
.selectAll()
|
|
13
|
+
.executeTakeFirst() as PostJsonResponse | undefined
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Fetch all posts
|
|
18
|
+
*/
|
|
19
|
+
export async function fetchAll(): Promise<PostJsonResponse[]> {
|
|
20
|
+
const db = await getDb()
|
|
21
|
+
return await db.selectFrom('posts').selectAll().execute() as PostJsonResponse[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Fetch posts by status
|
|
26
|
+
*/
|
|
27
|
+
export async function fetchByStatus(status: 'published' | 'draft' | 'archived'): Promise<PostJsonResponse[]> {
|
|
28
|
+
const db = await getDb()
|
|
29
|
+
return await db
|
|
30
|
+
.selectFrom('posts')
|
|
31
|
+
.where('status', '=', status)
|
|
32
|
+
.selectAll()
|
|
33
|
+
.execute() as PostJsonResponse[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Fetch posts by category
|
|
38
|
+
*/
|
|
39
|
+
export async function fetchByCategory(category: string): Promise<PostJsonResponse[]> {
|
|
40
|
+
const db = await getDb()
|
|
41
|
+
return await db
|
|
42
|
+
.selectFrom('posts')
|
|
43
|
+
.where('category', '=', category)
|
|
44
|
+
.selectAll()
|
|
45
|
+
.execute() as PostJsonResponse[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Fetch posts by author
|
|
50
|
+
*/
|
|
51
|
+
export async function fetchByAuthor(author: string): Promise<PostJsonResponse[]> {
|
|
52
|
+
const db = await getDb()
|
|
53
|
+
return await db
|
|
54
|
+
.selectFrom('posts')
|
|
55
|
+
.where('author', '=', author)
|
|
56
|
+
.selectAll()
|
|
57
|
+
.execute() as PostJsonResponse[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Fetch posts with minimum views
|
|
62
|
+
*/
|
|
63
|
+
export async function fetchByMinViews(minViews: number): Promise<PostJsonResponse[]> {
|
|
64
|
+
const db = await getDb()
|
|
65
|
+
return await db
|
|
66
|
+
.selectFrom('posts')
|
|
67
|
+
.where('views', '>=', minViews)
|
|
68
|
+
.selectAll()
|
|
69
|
+
.execute() as PostJsonResponse[]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Fetch posts published after a specific date
|
|
74
|
+
*/
|
|
75
|
+
export async function fetchPublishedAfter(timestamp: number): Promise<PostJsonResponse[]> {
|
|
76
|
+
const db = await getDb()
|
|
77
|
+
return await db
|
|
78
|
+
.selectFrom('posts')
|
|
79
|
+
.where('published_at', '>', timestamp)
|
|
80
|
+
.selectAll()
|
|
81
|
+
.execute() as PostJsonResponse[]
|
|
82
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export {
|
|
2
|
+
bulkDestroy,
|
|
3
|
+
destroy,
|
|
4
|
+
} from './destroy'
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
fetchAll,
|
|
8
|
+
fetchByAuthor,
|
|
9
|
+
fetchByCategory,
|
|
10
|
+
fetchById,
|
|
11
|
+
fetchByMinViews,
|
|
12
|
+
fetchByStatus,
|
|
13
|
+
fetchPublishedAfter,
|
|
14
|
+
} from './fetch'
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
attach,
|
|
18
|
+
detach,
|
|
19
|
+
store,
|
|
20
|
+
sync,
|
|
21
|
+
} from './store'
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
update,
|
|
25
|
+
} from './update'
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
type PostJsonResponse = ModelRow<typeof Post>
|
|
2
|
+
type NewPost = NewModelData<typeof Post>
|
|
3
|
+
import { randomUUIDv7 } from 'bun'
|
|
4
|
+
import { getDb } from '../database'
|
|
5
|
+
import { formatDate } from '@stacksjs/orm'
|
|
6
|
+
|
|
7
|
+
export const POST_STATUS_DRAFT = 'Draft'
|
|
8
|
+
export const POST_STATUS_PUBLISHED = 'Published'
|
|
9
|
+
export const POST_STATUS_ARCHIVED = 'Archived'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Create a new post
|
|
13
|
+
*
|
|
14
|
+
* @param data The post data to create
|
|
15
|
+
* @returns The created post record
|
|
16
|
+
*/
|
|
17
|
+
export async function store(data: NewPost & { body?: string, category?: string }): Promise<PostJsonResponse & { body?: string, category?: string }> {
|
|
18
|
+
const db = await getDb()
|
|
19
|
+
try {
|
|
20
|
+
if (!data.title || (typeof data.title === 'string' && data.title.trim() === '')) {
|
|
21
|
+
throw new Error('Post title is required')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Map body -> content if body is provided and content is not
|
|
25
|
+
const content = data.content || data.body
|
|
26
|
+
|
|
27
|
+
const d = data as Record<string, unknown>
|
|
28
|
+
const postData: Record<string, unknown> = {
|
|
29
|
+
author_id: d.author_id,
|
|
30
|
+
uuid: randomUUIDv7(),
|
|
31
|
+
title: data.title,
|
|
32
|
+
poster: data.poster,
|
|
33
|
+
content,
|
|
34
|
+
body: data.body,
|
|
35
|
+
category: data.category,
|
|
36
|
+
excerpt: data.excerpt,
|
|
37
|
+
is_featured: d.is_featured ? Date.now() : undefined,
|
|
38
|
+
views: data.views || 0,
|
|
39
|
+
published_at: d.published_at || Date.now(),
|
|
40
|
+
status: data.status || POST_STATUS_DRAFT,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const result = await db
|
|
44
|
+
.insertInto('posts')
|
|
45
|
+
.values(postData)
|
|
46
|
+
.returningAll()
|
|
47
|
+
.executeTakeFirst()
|
|
48
|
+
|
|
49
|
+
if (!result)
|
|
50
|
+
throw new Error('Failed to create post')
|
|
51
|
+
|
|
52
|
+
return result as PostJsonResponse & { body?: string, category?: string }
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error instanceof Error)
|
|
56
|
+
throw new TypeError(`Failed to create post: ${error.message}`)
|
|
57
|
+
|
|
58
|
+
throw error
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Attach related records to a post through a pivot table
|
|
64
|
+
*
|
|
65
|
+
* @param postId The ID of the post to attach records to
|
|
66
|
+
* @param tableName The name of the pivot table (e.g., 'categorizable_models', 'taggable')
|
|
67
|
+
* @param ids Array of IDs to attach
|
|
68
|
+
* @returns Promise<void>
|
|
69
|
+
*/
|
|
70
|
+
export async function attach(
|
|
71
|
+
postId: number,
|
|
72
|
+
tableName: 'categorizable_models' | 'taggable_models',
|
|
73
|
+
ids: number[],
|
|
74
|
+
): Promise<void> {
|
|
75
|
+
const db = await getDb()
|
|
76
|
+
try {
|
|
77
|
+
// Get the foreign key names based on the table name
|
|
78
|
+
const postForeignKey = tableName === 'categorizable_models' ? 'categorizable_id' : 'taggable_id'
|
|
79
|
+
const postTypeField = tableName === 'categorizable_models' ? 'categorizable_type' : 'taggable_type'
|
|
80
|
+
|
|
81
|
+
const tablePrimary = tableName === 'categorizable_models' ? 'category_id' : 'tag_id'
|
|
82
|
+
|
|
83
|
+
// Prepare the data for insertion
|
|
84
|
+
const pivotData = ids.map((id: number) => ({
|
|
85
|
+
[tablePrimary]: id,
|
|
86
|
+
[postForeignKey]: postId,
|
|
87
|
+
[postTypeField]: 'posts',
|
|
88
|
+
created_at: formatDate(new Date()),
|
|
89
|
+
updated_at: formatDate(new Date()),
|
|
90
|
+
}))
|
|
91
|
+
|
|
92
|
+
for (const data of pivotData) {
|
|
93
|
+
await db
|
|
94
|
+
.insertInto(tableName)
|
|
95
|
+
.values(data)
|
|
96
|
+
.execute()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (error instanceof Error)
|
|
101
|
+
throw new TypeError(`Failed to attach records: ${error.message}`)
|
|
102
|
+
|
|
103
|
+
throw error
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Detach related records from a post through a pivot table
|
|
109
|
+
*
|
|
110
|
+
* @param postId The ID of the post to detach records from
|
|
111
|
+
* @param tableName The name of the pivot table (e.g., 'categorizable_models', 'taggable')
|
|
112
|
+
* @param ids Optional array of IDs to detach. If not provided, all related records will be detached
|
|
113
|
+
* @returns Promise<void>
|
|
114
|
+
*/
|
|
115
|
+
export async function detach(
|
|
116
|
+
postId: number,
|
|
117
|
+
tableName: 'categorizable_models' | 'taggable_models',
|
|
118
|
+
ids?: number[],
|
|
119
|
+
): Promise<void> {
|
|
120
|
+
const db = await getDb()
|
|
121
|
+
try {
|
|
122
|
+
// Get the foreign key names based on the table name
|
|
123
|
+
const postForeignKey = tableName === 'categorizable_models' ? 'categorizable_id' : 'taggable_id'
|
|
124
|
+
const postTypeField = tableName === 'categorizable_models' ? 'categorizable_type' : 'taggable_type'
|
|
125
|
+
|
|
126
|
+
// Build the delete query
|
|
127
|
+
let query = db
|
|
128
|
+
.deleteFrom(tableName)
|
|
129
|
+
.where(postForeignKey, '=', postId)
|
|
130
|
+
.where(postTypeField, '=', 'posts')
|
|
131
|
+
|
|
132
|
+
if (ids) {
|
|
133
|
+
query = query.where('id', 'in', ids)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await query.execute()
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (error instanceof Error)
|
|
140
|
+
throw new TypeError(`Failed to detach records: ${error.message}`)
|
|
141
|
+
|
|
142
|
+
throw error
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Synchronize related records for a post through a pivot table
|
|
148
|
+
* This will detach relationships not in the new set and attach only new relationships
|
|
149
|
+
*
|
|
150
|
+
* @param postId The ID of the post to sync records for
|
|
151
|
+
* @param tableName The name of the pivot table (e.g., 'categorizable_models', 'taggable')
|
|
152
|
+
* @param ids Array of IDs to sync
|
|
153
|
+
* @returns Promise<void>
|
|
154
|
+
*/
|
|
155
|
+
export async function sync(
|
|
156
|
+
postId: number,
|
|
157
|
+
tableName: 'categorizable_models' | 'taggable_models',
|
|
158
|
+
ids: number[],
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
const db = await getDb()
|
|
161
|
+
try {
|
|
162
|
+
// Get the foreign key names based on the table name
|
|
163
|
+
const postForeignKey = tableName === 'categorizable_models' ? 'categorizable_id' : 'taggable_id'
|
|
164
|
+
const postTypeField = tableName === 'categorizable_models' ? 'categorizable_type' : 'taggable_type'
|
|
165
|
+
|
|
166
|
+
// Get existing relationships
|
|
167
|
+
const existingRelations = await db
|
|
168
|
+
.selectFrom(tableName)
|
|
169
|
+
.select(['id'])
|
|
170
|
+
.where(postForeignKey, '=', postId)
|
|
171
|
+
.where(postTypeField, '=', 'posts')
|
|
172
|
+
.execute()
|
|
173
|
+
|
|
174
|
+
const existingIds = existingRelations.map((rel: Record<string, unknown>) => rel.id as number)
|
|
175
|
+
|
|
176
|
+
// Find IDs to remove (in existing but not in new set)
|
|
177
|
+
const idsToRemove = existingIds.filter((id: number) => !ids.includes(id))
|
|
178
|
+
|
|
179
|
+
// Find IDs to add (in new set but not in existing)
|
|
180
|
+
const idsToAdd = ids.filter(id => !existingIds.includes(id))
|
|
181
|
+
|
|
182
|
+
// Remove relationships that are no longer needed
|
|
183
|
+
if (idsToRemove.length > 0) {
|
|
184
|
+
await db
|
|
185
|
+
.deleteFrom(tableName)
|
|
186
|
+
.where(postForeignKey, '=', postId)
|
|
187
|
+
.where(postTypeField, '=', 'posts')
|
|
188
|
+
.where('id', 'in', idsToRemove)
|
|
189
|
+
.execute()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Add new relationships
|
|
193
|
+
if (idsToAdd.length > 0) {
|
|
194
|
+
const pivotData = idsToAdd.map(() => ({
|
|
195
|
+
[postForeignKey]: postId,
|
|
196
|
+
[postTypeField]: 'posts',
|
|
197
|
+
created_at: formatDate(new Date()),
|
|
198
|
+
updated_at: formatDate(new Date()),
|
|
199
|
+
}))
|
|
200
|
+
|
|
201
|
+
await db
|
|
202
|
+
.insertInto(tableName)
|
|
203
|
+
.values(pivotData)
|
|
204
|
+
.execute()
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
if (error instanceof Error)
|
|
209
|
+
throw new TypeError(`Failed to sync records: ${error.message}`)
|
|
210
|
+
|
|
211
|
+
throw error
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
type PostJsonResponse = ModelRow<typeof Post>
|
|
2
|
+
type PostUpdate = UpdateModelData<typeof Post>
|
|
3
|
+
import { getDb } from '../database'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Update a post
|
|
7
|
+
*
|
|
8
|
+
* @param id The id of the post to update
|
|
9
|
+
* @param data The post data to update
|
|
10
|
+
* @returns The updated post record
|
|
11
|
+
*/
|
|
12
|
+
export async function update(id: number, data: Partial<PostUpdate>): Promise<PostJsonResponse> {
|
|
13
|
+
const db = await getDb()
|
|
14
|
+
try {
|
|
15
|
+
if (data.title !== undefined && typeof data.title === 'string' && data.title.trim() === '') {
|
|
16
|
+
throw new Error('Post title cannot be empty')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const validStatuses = ['published', 'draft', 'archived']
|
|
20
|
+
if (data.status !== undefined && typeof data.status === 'string' && !validStatuses.includes(data.status)) {
|
|
21
|
+
throw new Error(`Invalid post status: ${data.status}`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (data.views !== undefined && typeof data.views === 'number' && data.views < 0) {
|
|
25
|
+
throw new Error('Views count cannot be negative')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const result = await db
|
|
29
|
+
.updateTable('posts')
|
|
30
|
+
.set(data)
|
|
31
|
+
.where('id', '=', id)
|
|
32
|
+
.returningAll()
|
|
33
|
+
.executeTakeFirst()
|
|
34
|
+
|
|
35
|
+
if (!result)
|
|
36
|
+
throw new Error('Failed to update post')
|
|
37
|
+
|
|
38
|
+
return result as PostJsonResponse
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error instanceof Error)
|
|
42
|
+
throw new TypeError(`Failed to update post: ${error.message}`)
|
|
43
|
+
|
|
44
|
+
throw error
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { getDb } from '../database'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Delete a tag by its ID
|
|
5
|
+
*
|
|
6
|
+
* @param id The ID of the tag to delete
|
|
7
|
+
* @returns void
|
|
8
|
+
*/
|
|
9
|
+
export async function destroy(id: number): Promise<void> {
|
|
10
|
+
const db = await getDb()
|
|
11
|
+
// First check if the tag exists
|
|
12
|
+
const tag = await db
|
|
13
|
+
.selectFrom('taggables')
|
|
14
|
+
.where('id', '=', id)
|
|
15
|
+
.selectAll()
|
|
16
|
+
.executeTakeFirst()
|
|
17
|
+
|
|
18
|
+
if (!tag) {
|
|
19
|
+
throw new Error(`Tag with ID ${id} not found`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
await db
|
|
23
|
+
.deleteFrom('taggables')
|
|
24
|
+
.where('id', '=', id)
|
|
25
|
+
.executeTakeFirst()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Delete multiple tags by their IDs
|
|
30
|
+
*
|
|
31
|
+
* @param ids An array of tag IDs to delete
|
|
32
|
+
* @returns void
|
|
33
|
+
*/
|
|
34
|
+
export async function bulkDestroy(ids: number[]): Promise<void> {
|
|
35
|
+
const db = await getDb()
|
|
36
|
+
try {
|
|
37
|
+
await db
|
|
38
|
+
.deleteFrom('taggables')
|
|
39
|
+
.where('id', 'in', ids)
|
|
40
|
+
.execute()
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error instanceof Error) {
|
|
44
|
+
throw new TypeError(`Failed to delete tags: ${error.message}`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
throw error
|
|
48
|
+
}
|
|
49
|
+
}
|