@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.
@@ -0,0 +1,319 @@
1
+ import { getDb } from '../database'
2
+ import { formatDate } from '@stacksjs/orm'
3
+
4
+ export interface Commentable {
5
+ id?: number
6
+ title: string
7
+ body: string
8
+ status: string
9
+ approved_at: number | null
10
+ rejected_at: number | null
11
+ commentables_id: number
12
+ commentables_type: string
13
+ reports_count?: number
14
+ reported_at?: number | null
15
+ upvotes_count?: number
16
+ downvotes_count?: number
17
+ user_id: number | null
18
+ created_at?: string
19
+ updated_at?: string | null
20
+ }
21
+
22
+ export type CommentStatus = 'approved' | 'pending' | 'spam'
23
+
24
+ export async function fetchComments(options: {
25
+ status?: Commentable['status']
26
+ commentables_id?: number
27
+ commentables_type?: string
28
+ limit?: number
29
+ offset?: number
30
+ } = {}): Promise<Commentable[]> {
31
+ const db = await getDb()
32
+ let query = db.selectFrom('commentables') as any
33
+
34
+ if (options.status)
35
+ query = query.where('status', '=', options.status)
36
+
37
+ if (options.commentables_id)
38
+ query = query.where('commentables_id', '=', options.commentables_id)
39
+
40
+ if (options.commentables_type)
41
+ query = query.where('commentables_type', '=', options.commentables_type)
42
+
43
+ if (options.limit)
44
+ query = query.limit(options.limit)
45
+
46
+ if (options.offset)
47
+ query = query.offset(options.offset)
48
+
49
+ return query.selectAll().execute() as Promise<Commentable[]>
50
+ }
51
+
52
+ export async function fetchCommentById(id: number): Promise<Commentable | undefined> {
53
+ const db = await getDb()
54
+ return db
55
+ .selectFrom('commentables')
56
+ .where('id', '=', id)
57
+ .selectAll()
58
+ .executeTakeFirst() as unknown as Promise<Commentable | undefined>
59
+ }
60
+
61
+ export async function fetchCommentsByCommentables(
62
+ commentables_id: number,
63
+ commentables_type: string,
64
+ options: { status?: Commentable['status'], limit?: number, offset?: number } = {},
65
+ ): Promise<Commentable[]> {
66
+ const db = await getDb()
67
+ let query = db
68
+ .selectFrom('commentables')
69
+ .where('commentables_id', '=', commentables_id)
70
+ .where('commentables_type', '=', commentables_type) as any
71
+
72
+ if (options.status)
73
+ query = query.where('status', '=', options.status)
74
+
75
+ if (options.limit)
76
+ query = query.limit(options.limit)
77
+
78
+ if (options.offset)
79
+ query = query.offset(options.offset)
80
+
81
+ return query.selectAll().execute() as Promise<Commentable[]>
82
+ }
83
+
84
+ /**
85
+ * Fetch comment counts for different time periods
86
+ *
87
+ * @param days The number of days to look back (e.g., 7, 14, 30, 60, 90)
88
+ * @returns The count of comments within the specified time period
89
+ */
90
+ export async function fetchCommentCountByPeriod(days: number): Promise<number> {
91
+ const db = await getDb()
92
+ try {
93
+ const result = await db
94
+ .selectFrom('commentables')
95
+ .where('created_at', '>=', new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString())
96
+ .count()
97
+
98
+ return Number(result) || 0
99
+ }
100
+ catch (error) {
101
+ if (error instanceof Error) {
102
+ throw new TypeError(`Failed to fetch comment count: ${error.message}`)
103
+ }
104
+
105
+ throw error
106
+ }
107
+ }
108
+
109
+ export async function fetchCommentsByStatus(status: CommentStatus, options: { limit?: number, offset?: number } = {}): Promise<Commentable[]> {
110
+ const db = await getDb()
111
+ try {
112
+ let query = db
113
+ .selectFrom('commentables')
114
+ .where('status', '=', status) as any
115
+
116
+ if (options.limit)
117
+ query = query.limit(options.limit)
118
+
119
+ if (options.offset)
120
+ query = query.offset(options.offset)
121
+
122
+ return query.selectAll().execute() as Promise<Commentable[]>
123
+ }
124
+ catch (error) {
125
+ if (error instanceof Error) {
126
+ throw new TypeError(`Failed to fetch comments by status: ${error.message}`)
127
+ }
128
+
129
+ throw error
130
+ }
131
+ }
132
+
133
+ export async function calculateApprovalRate(): Promise<{ approved: number, total: number, rate: number }> {
134
+ const db = await getDb()
135
+ try {
136
+ const [approvedCount, totalCount] = await Promise.all([
137
+ db
138
+ .selectFrom('commentables')
139
+ .where('status', '=', 'approved')
140
+ .count(),
141
+ db
142
+ .selectFrom('commentables')
143
+ .count(),
144
+ ])
145
+
146
+ const approved = Number(approvedCount || 0)
147
+ const total = Number(totalCount || 0)
148
+ const rate = total > 0 ? (approved / total) * 100 : 0
149
+
150
+ return {
151
+ approved,
152
+ total,
153
+ rate,
154
+ }
155
+ }
156
+ catch (error) {
157
+ if (error instanceof Error) {
158
+ throw new TypeError(`Failed to calculate approval rate: ${error.message}`)
159
+ }
160
+
161
+ throw error
162
+ }
163
+ }
164
+
165
+ export interface PostWithCommentCount {
166
+ id: number
167
+ title: string
168
+ comment_count: number
169
+ }
170
+
171
+ export interface DateRange {
172
+ startDate: Date
173
+ endDate: Date
174
+ }
175
+
176
+ export async function fetchPostsWithMostComments(dateRange: DateRange, options: { limit?: number } = {}): Promise<PostWithCommentCount[]> {
177
+ const db = await getDb()
178
+ try {
179
+ let query = db
180
+ .selectFrom('posts')
181
+ .leftJoin('commentables', 'posts.id', '=', 'commentables.commentables_id')
182
+ .where('commentables.created_at', '>=', formatDate(dateRange.startDate))
183
+ .where('commentables.created_at', '<=', formatDate(dateRange.endDate))
184
+ .select([
185
+ 'posts.id',
186
+ 'posts.title',
187
+ ])
188
+ .groupBy('posts.id', 'posts.title')
189
+ .orderBy('posts.id', 'desc') as any
190
+
191
+ if (options.limit)
192
+ query = query.limit(options.limit)
193
+
194
+ const results = await query.execute()
195
+
196
+ return (results as Record<string, unknown>[]).map((row: Record<string, unknown>) => ({
197
+ id: Number(row.id),
198
+ title: String(row.title),
199
+ comment_count: Number(row.comment_count || 0),
200
+ }))
201
+ }
202
+ catch (error) {
203
+ if (error instanceof Error) {
204
+ throw new TypeError(`Failed to fetch posts with most comments: ${error.message}`)
205
+ }
206
+
207
+ throw error
208
+ }
209
+ }
210
+
211
+ export interface BarGraphData {
212
+ labels: string[]
213
+ values: number[]
214
+ }
215
+
216
+ export async function fetchCommentCountBarGraph(dateRange: DateRange, options: { limit?: number } = {}): Promise<BarGraphData> {
217
+ try {
218
+ const posts = await fetchPostsWithMostComments(dateRange, options)
219
+
220
+ return {
221
+ labels: posts.map((post: any) => post.title),
222
+ values: posts.map((post: any) => post.comment_count),
223
+ }
224
+ }
225
+ catch (error) {
226
+ if (error instanceof Error) {
227
+ throw new TypeError(`Failed to fetch bar graph data: ${error.message}`)
228
+ }
229
+
230
+ throw error
231
+ }
232
+ }
233
+
234
+ export interface DonutGraphData {
235
+ labels: string[]
236
+ values: number[]
237
+ percentages: number[]
238
+ }
239
+
240
+ export async function fetchStatusDistributionDonut(dateRange: DateRange): Promise<DonutGraphData> {
241
+ const db = await getDb()
242
+ try {
243
+ const results = await db
244
+ .selectFrom('commentables')
245
+ .where('created_at', '>=', formatDate(dateRange.startDate))
246
+ .where('created_at', '<=', formatDate(dateRange.endDate))
247
+ .select([
248
+ 'status',
249
+ ])
250
+ .groupBy('status')
251
+ .execute()
252
+
253
+ const typedResults = results as Record<string, unknown>[]
254
+ const total = typedResults.reduce((sum: number, row: Record<string, unknown>) => sum + Number(row.count || 0), 0)
255
+ const labels = typedResults.map((row: Record<string, unknown>) => String(row.status))
256
+ const values = typedResults.map((row: Record<string, unknown>) => Number(row.count || 0))
257
+ const percentages = values.map((value: number) => total > 0 ? (value / total) * 100 : 0)
258
+
259
+ return {
260
+ labels,
261
+ values,
262
+ percentages,
263
+ }
264
+ }
265
+ catch (error) {
266
+ if (error instanceof Error)
267
+ throw new TypeError(`Failed to fetch status distribution: ${error.message}`)
268
+
269
+ throw error
270
+ }
271
+ }
272
+
273
+ export interface LineGraphData {
274
+ labels: string[]
275
+ values: number[]
276
+ }
277
+
278
+ export async function fetchMonthlyCommentCounts(dateRange: DateRange): Promise<LineGraphData> {
279
+ const db = await getDb()
280
+ try {
281
+ const results = await db
282
+ .selectFrom('commentables')
283
+ .where('created_at', '>=', formatDate(dateRange.startDate))
284
+ .where('created_at', '<=', formatDate(dateRange.endDate))
285
+ .select([
286
+ 'created_at',
287
+ ])
288
+ .groupBy('created_at')
289
+ .orderBy('created_at', 'asc')
290
+ .execute()
291
+
292
+ // Group results by year and month
293
+ const monthlyCounts = new Map<string, number>()
294
+ ;(results as Record<string, unknown>[]).forEach((row: Record<string, unknown>) => {
295
+ if (!row.created_at)
296
+ return
297
+ const date = new Date(row.created_at as string)
298
+ const key = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`
299
+ monthlyCounts.set(key, (monthlyCounts.get(key) || 0) + Number(row.count || 0))
300
+ })
301
+
302
+ // Convert to arrays for the graph
303
+ const labels: string[] = []
304
+ const values: number[] = []
305
+ monthlyCounts.forEach((count: any, key: any) => {
306
+ const [year, month] = key.split('-')
307
+ labels.push(new Date(Number(year), Number(month) - 1).toLocaleString('default', { month: 'short', year: 'numeric' }))
308
+ values.push(count)
309
+ })
310
+
311
+ return { labels, values }
312
+ }
313
+ catch (error) {
314
+ if (error instanceof Error)
315
+ throw new TypeError(`Failed to fetch monthly comment counts: ${error.message}`)
316
+
317
+ throw error
318
+ }
319
+ }
@@ -0,0 +1,25 @@
1
+ export {
2
+ bulkDestroy,
3
+ destroy,
4
+ } from './destroy'
5
+
6
+ export {
7
+ calculateApprovalRate,
8
+ fetchCommentById,
9
+ fetchCommentCountBarGraph,
10
+ fetchCommentCountByPeriod,
11
+ fetchComments,
12
+ fetchCommentsByCommentables,
13
+ fetchCommentsByStatus,
14
+ fetchMonthlyCommentCounts,
15
+ fetchPostsWithMostComments,
16
+ fetchStatusDistributionDonut,
17
+ } from './fetch'
18
+
19
+ export {
20
+ store,
21
+ } from './store'
22
+
23
+ export {
24
+ update,
25
+ } from './update'
@@ -0,0 +1,156 @@
1
+ import type { CommentablesTable } from '@stacksjs/orm'
2
+ import { getDb } from '../database'
3
+
4
+ export interface CreateCommentInput {
5
+ title: string
6
+ body: string
7
+ commentables_id: number
8
+ commentables_type: string
9
+ }
10
+
11
+ export interface UpdateCommentInput {
12
+ title?: string
13
+ body?: string
14
+ status?: string
15
+ }
16
+
17
+ export async function createComment(data: CreateCommentInput): Promise<CommentablesTable> {
18
+ const db = await getDb()
19
+ const now = new Date()
20
+
21
+ const commentData = {
22
+ title: data.title,
23
+ body: data.body,
24
+ commentables_id: data.commentables_id,
25
+ commentables_type: data.commentables_type,
26
+ status: 'pending',
27
+ created_at: now.toISOString(),
28
+ }
29
+
30
+ return db
31
+ .insertInto('commentables')
32
+ .values(commentData)
33
+ .returningAll()
34
+ .executeTakeFirstOrThrow() as unknown as Promise<CommentablesTable>
35
+ }
36
+
37
+ export async function updateComment(
38
+ id: number,
39
+ input: UpdateCommentInput,
40
+ ): Promise<CommentablesTable> {
41
+ const db = await getDb()
42
+ return db
43
+ .updateTable('commentables')
44
+ .set({
45
+ ...input,
46
+ updated_at: new Date().toISOString(),
47
+ })
48
+ .where('id', '=', id)
49
+ .returningAll()
50
+ .executeTakeFirstOrThrow() as unknown as Promise<CommentablesTable>
51
+ }
52
+
53
+ export async function approveComment(id: number): Promise<CommentablesTable> {
54
+ const db = await getDb()
55
+ return db
56
+ .updateTable('commentables')
57
+ .set({
58
+ status: 'approved',
59
+ approved_at: Date.now(),
60
+ updated_at: new Date().toISOString(),
61
+ })
62
+ .where('id', '=', id)
63
+ .returningAll()
64
+ .executeTakeFirstOrThrow() as unknown as Promise<CommentablesTable>
65
+ }
66
+
67
+ export async function rejectComment(id: number): Promise<CommentablesTable> {
68
+ const db = await getDb()
69
+ return db
70
+ .updateTable('commentables')
71
+ .set({
72
+ status: 'rejected',
73
+ rejected_at: Date.now(),
74
+ updated_at: new Date().toISOString(),
75
+ })
76
+ .where('id', '=', id)
77
+ .returningAll()
78
+ .executeTakeFirstOrThrow() as unknown as Promise<CommentablesTable>
79
+ }
80
+
81
+ export async function deleteComment(id: number): Promise<void> {
82
+ const db = await getDb()
83
+ await db
84
+ .deleteFrom('commentables')
85
+ .where('id', '=', id)
86
+ .execute()
87
+ }
88
+
89
+ interface CommentStore {
90
+ title: string
91
+ body: string
92
+ status: string
93
+ user_id: number
94
+ commentables_id: number
95
+ commentables_type: string
96
+ is_active?: boolean | null
97
+ approved_at?: number | null
98
+ rejected_at?: number | null
99
+ }
100
+
101
+ /**
102
+ * Create a new comment
103
+ *
104
+ * @param data The comment data to store
105
+ * @returns The newly created comment record
106
+ */
107
+ export async function store(data: CommentStore): Promise<CommentablesTable> {
108
+ const db = await getDb()
109
+ try {
110
+ if (!data.title || data.title.trim() === '') {
111
+ throw new Error('Comment title is required')
112
+ }
113
+
114
+ if (!data.body || data.body.trim() === '') {
115
+ throw new Error('Comment body is required')
116
+ }
117
+
118
+ if (!data.commentables_type || data.commentables_type.trim() === '') {
119
+ throw new Error('Comment commentables_type is required')
120
+ }
121
+
122
+ const validStatuses = ['pending', 'approved', 'rejected']
123
+ if (data.status && !validStatuses.includes(data.status)) {
124
+ throw new Error(`Invalid comment status: ${data.status}`)
125
+ }
126
+
127
+ const commentData = {
128
+ title: data.title,
129
+ body: data.body,
130
+ status: data.status,
131
+ commentables_id: data.commentables_id,
132
+ commentables_type: data.commentables_type,
133
+ user_id: data.user_id,
134
+ is_active: data.is_active,
135
+ }
136
+
137
+ const result = await db
138
+ .insertInto('commentables')
139
+ .values(commentData)
140
+ .returningAll()
141
+ .executeTakeFirst()
142
+
143
+ if (!result) {
144
+ throw new Error('Failed to create comment')
145
+ }
146
+
147
+ return result as unknown as CommentablesTable
148
+ }
149
+ catch (error) {
150
+ if (error instanceof Error) {
151
+ throw new TypeError(`Failed to store comment: ${error.message}`)
152
+ }
153
+
154
+ throw error
155
+ }
156
+ }
@@ -0,0 +1,75 @@
1
+ import type { CommentablesTable } from '@stacksjs/orm'
2
+ import { getDb } from '../database'
3
+ import { formatDate } from '@stacksjs/orm'
4
+
5
+ interface CommentUpdate {
6
+ title?: string
7
+ body?: string
8
+ status?: string
9
+ approved_at?: number | null
10
+ rejected_at?: number | null
11
+ commentables_id?: number
12
+ commentables_type?: string
13
+ user_id?: number | null
14
+ updated_at?: string | null
15
+ }
16
+
17
+ /**
18
+ * Update a comment by ID
19
+ *
20
+ * @param id The ID of the comment to update
21
+ * @param data The updated comment data
22
+ * @returns The updated comment record
23
+ */
24
+ export async function update(id: number, data: CommentUpdate): Promise<CommentablesTable | undefined> {
25
+ const db = await getDb()
26
+ try {
27
+ if (data.title !== undefined && data.title.trim() === '') {
28
+ throw new Error('Comment title cannot be empty')
29
+ }
30
+
31
+ if (data.body !== undefined && data.body.trim() === '') {
32
+ throw new Error('Comment body cannot be empty')
33
+ }
34
+
35
+ const validStatuses = ['pending', 'approved', 'rejected']
36
+ if (data.status !== undefined && !validStatuses.includes(data.status)) {
37
+ throw new Error(`Invalid comment status: ${data.status}`)
38
+ }
39
+
40
+ // Only include fields that are explicitly provided
41
+ const commentData: Record<string, unknown> = {
42
+ updated_at: formatDate(new Date()),
43
+ }
44
+
45
+ if (data.title !== undefined) commentData.title = data.title
46
+ if (data.body !== undefined) commentData.body = data.body
47
+ if (data.status !== undefined) commentData.status = data.status
48
+ if (data.commentables_id !== undefined) commentData.commentables_id = data.commentables_id
49
+ if (data.commentables_type !== undefined) commentData.commentables_type = data.commentables_type
50
+ if (data.approved_at !== undefined) commentData.approved_at = data.approved_at
51
+ if (data.rejected_at !== undefined) commentData.rejected_at = data.rejected_at
52
+
53
+ // Update the comment record
54
+ await db
55
+ .updateTable('commentables')
56
+ .set(commentData)
57
+ .where('id', '=', id)
58
+ .execute()
59
+
60
+ const updatedComment = await db
61
+ .selectFrom('commentables')
62
+ .where('id', '=', id)
63
+ .selectAll()
64
+ .executeTakeFirst()
65
+
66
+ return updatedComment as unknown as CommentablesTable | undefined
67
+ }
68
+ catch (error) {
69
+ if (error instanceof Error) {
70
+ throw new TypeError(`Failed to update comment: ${error.message}`)
71
+ }
72
+
73
+ throw error
74
+ }
75
+ }
@@ -0,0 +1,9 @@
1
+ type StacksDatabase = typeof import('@stacksjs/database').db
2
+
3
+ let dbPromise: Promise<StacksDatabase> | undefined
4
+
5
+ export async function getDb(): Promise<StacksDatabase> {
6
+ dbPromise ??= import('@stacksjs/database').then(module => module.db)
7
+
8
+ return dbPromise
9
+ }
package/src/index.ts ADDED
@@ -0,0 +1,42 @@
1
+ import * as authors from './authors'
2
+ import * as postCategories from './categorizables'
3
+ import * as comments from './commentables'
4
+ import * as pages from './pages'
5
+ import * as posts from './posts'
6
+ import * as tags from './taggables'
7
+
8
+ type PostsModule = typeof posts
9
+ type PostCategoriesModule = typeof postCategories
10
+ type TagsModule = typeof tags
11
+ type CommentsModule = typeof comments
12
+ type AuthorsModule = typeof authors
13
+ type PagesModule = typeof pages
14
+
15
+ export interface CmsNamespace {
16
+ posts: PostsModule
17
+ postCategories: PostCategoriesModule
18
+ tags: TagsModule
19
+ comments: CommentsModule
20
+ authors: AuthorsModule
21
+ pages: PagesModule
22
+ }
23
+
24
+ export const cms: CmsNamespace = {
25
+ posts,
26
+ postCategories,
27
+ tags,
28
+ comments,
29
+ authors,
30
+ pages,
31
+ }
32
+
33
+ export default cms
34
+
35
+ export {
36
+ authors,
37
+ postCategories as categorizable,
38
+ comments,
39
+ pages,
40
+ posts,
41
+ tags,
42
+ }
@@ -0,0 +1,64 @@
1
+ import { getDb } from '../database'
2
+ import { fetchById } from './fetch'
3
+
4
+ /**
5
+ * Delete a page by ID
6
+ *
7
+ * @param id The ID of the page 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 page exists
14
+ const page = await fetchById(id)
15
+
16
+ if (!page) {
17
+ throw new Error(`Page with ID ${id} not found`)
18
+ }
19
+
20
+ // Delete the page
21
+ const result = await db
22
+ .deleteFrom('pages')
23
+ .where('id', '=', id)
24
+ .executeTakeFirst()
25
+
26
+ return result.numDeletedRows > 0
27
+ }
28
+ catch (error) {
29
+ if (error instanceof Error) {
30
+ throw new TypeError(`Failed to delete page: ${error.message}`)
31
+ }
32
+
33
+ throw error
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Bulk delete multiple pages
39
+ *
40
+ * @param ids Array of page IDs to delete
41
+ * @returns Number of pages successfully deleted
42
+ */
43
+ export async function bulkDestroy(ids: number[]): Promise<number> {
44
+ const db = await getDb()
45
+ if (!ids.length)
46
+ return 0
47
+
48
+ try {
49
+ // Delete all pages in the array
50
+ const result = await db
51
+ .deleteFrom('pages')
52
+ .where('id', 'in', ids)
53
+ .executeTakeFirst()
54
+
55
+ return Number(result.numDeletedRows) || 0
56
+ }
57
+ catch (error) {
58
+ if (error instanceof Error) {
59
+ throw new TypeError(`Failed to delete pages in bulk: ${error.message}`)
60
+ }
61
+
62
+ throw error
63
+ }
64
+ }