news-cms-module 0.0.1 → 0.1.2

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/config/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  const path = require('path');
2
2
 
3
- // Konfigurasi Default untuk Package Anda
4
3
  const defaultConfig = {
5
4
  // Pengaturan Router
6
5
  adminRoutePrefix: '/admin', // Prefix default untuk route CRUD admin
@@ -54,7 +54,7 @@ class NewsController {
54
54
 
55
55
  // --- Route Admin (CRUD) ---
56
56
  async adminList(req, res) {
57
- // Logic ambil data dengan status DRAFT atau PUBLISHED untuk admin
57
+ //tambahkan ARCHIVE
58
58
  const { rows: posts } = await this.newsService.getAllPosts(req.query.page, 10, ['DRAFT', 'PUBLISHED']);
59
59
 
60
60
  res.status(200).json({
@@ -68,10 +68,8 @@ class NewsController {
68
68
 
69
69
  async createPost(req, res) {
70
70
  try {
71
- // Asumsi req.body.content adalah array of blocks [{blockType, contentValue}, ...]
72
71
  const { title, summary, authorId, status, contentBlocks } = req.body;
73
72
 
74
- // Slug harus dibuat sebelum disimpan
75
73
  const slug = title.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, '');
76
74
 
77
75
  const newNews = await this.newsService.createPost(
@@ -96,7 +94,68 @@ class NewsController {
96
94
  }
97
95
  }
98
96
 
99
- // ... Tambahkan updatePost, deletePost, dll.
97
+ async updatePost(req, res) {
98
+ try {
99
+ const { id } = req.params;
100
+ const { title, summary, authorId, status, contentBlocks } = req.body;
101
+
102
+ if (!id || !title || !contentBlocks) {
103
+ return res.status(400).json({ success: false, message: 'Missing required fields or ID.' });
104
+ }
105
+
106
+ const slug = title.toLowerCase().trim().replace(/ /g, '-').replace(/[^\w-]+/g, '');
107
+ const isPublished = status === 'PUBLISHED';
108
+
109
+ const updatedPost = await this.newsService.updatePost(
110
+ id,
111
+ {
112
+ title,
113
+ slug,
114
+ summary,
115
+ authorId,
116
+ status: status || 'DRAFT',
117
+ publishedAt: isPublished ? new Date() : null
118
+ },
119
+ contentBlocks
120
+ );
121
+
122
+ if (!updatedPost) {
123
+ return res.status(404).json({ success: false, message: 'News post not found for update.' });
124
+ }
125
+
126
+ res.status(200).json({
127
+ success: true,
128
+ message: 'Post updated successfully.',
129
+ data: updatedPost
130
+ });
131
+
132
+ } catch (error) {
133
+ console.error(error);
134
+ res.status(500).json({ success: false, message: 'Failed to update post.', error: error.message });
135
+ }
136
+ }
137
+
138
+
139
+ async deletePost(req, res) {
140
+ try {
141
+ const { id } = req.params;
142
+
143
+ const result = await this.newsService.deletePost(id);
144
+
145
+ if (result === 0) { // Sequelize destroy mengembalikan 0 jika tidak ada baris yang terpengaruh
146
+ return res.status(404).json({ success: false, message: 'News post not found for deletion.' });
147
+ }
148
+
149
+ res.status(200).json({
150
+ success: true,
151
+ message: `Post with ID ${id} deleted successfully.`
152
+ });
153
+ } catch (error) {
154
+ console.error(error);
155
+ res.status(500).json({ success: false, message: 'Failed to delete post.', error: error.message });
156
+ }
157
+ }
158
+
100
159
  }
101
160
 
102
161
  module.exports = NewsController;
@@ -5,14 +5,14 @@ class StatController {
5
5
  this.statService = statService;
6
6
  }
7
7
 
8
- // Middleware untuk pelacakan kunjungan
8
+ // Middleware untuk pelacakan kunjungan blum selesai
9
9
  async trackVisitMiddleware(req, res, next) {
10
10
  const { slug } = req.params;
11
11
 
12
12
  // Ambil post ID (idealnya dilakukan oleh service sebelum dipanggil)
13
13
  // Untuk demo, kita asumsikan kita punya newsId dari service atau middleware sebelumnya.
14
14
  // **REAL-WORLD:** Anda harus mencari newsId berdasarkan slug di sini atau di service.
15
- const newsId = 1; // Contoh: Asumsi newsId ditemukan
15
+ // const newsId = 1; // Contoh: Asumsi newsId ditemukan
16
16
 
17
17
  const sessionId = req.sessionID || req.ip; // Gunakan sesi atau IP untuk unique ID
18
18
 
@@ -20,7 +20,7 @@ class StatController {
20
20
  await this.statService.trackVisit(newsId, sessionId);
21
21
  }
22
22
 
23
- next(); // Lanjutkan ke controller NewsController.getDetail
23
+ next();
24
24
  }
25
25
 
26
26
  async getTrendingApi(req, res) {
package/index.js CHANGED
@@ -1,8 +1,3 @@
1
- const express = require('express');
2
- const initModels = require('./models'); // Mengimpor inisialisasi model dan koneksi DB
3
- const initServices = require('./services'); // Mengimpor semua Services (CRUD, Stat)
4
- const setupRoutes = require('./routes'); // Mengimpor fungsi setup router
5
-
6
1
  /**
7
2
  * Fungsi utama package yang di-export.
8
3
  * @param {object} dbConfig - Konfigurasi koneksi database dari aplikasi pengguna.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "news-cms-module",
3
- "version": "0.0.1",
3
+ "version": "0.1.2",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"
package/routes/index.js CHANGED
@@ -1,5 +1,3 @@
1
- // routes/index.js
2
-
3
1
  const express = require('express');
4
2
  const NewsController = require('../controllers/NewsController');
5
3
  const StatController = require('../controllers/StatController');
@@ -52,23 +50,15 @@ module.exports = (router, services, config) => {
52
50
  adminRouter.post('/create', newsController.createPost.bind(newsController));
53
51
 
54
52
  // PUT/PATCH /admin/update/:id (Memperbarui berita)
55
- // adminRouter.put('/update/:id', newsController.updatePost.bind(newsController));
53
+ adminRouter.put('/update/:id', newsController.updatePost.bind(newsController));
56
54
 
57
55
  // DELETE /admin/delete/:id (Menghapus berita)
58
- // adminRouter.delete('/delete/:id', newsController.deletePost.bind(newsController));
56
+ adminRouter.delete('/delete/:id', newsController.deletePost.bind(newsController));
59
57
 
60
58
  // Pasang router admin ke prefix yang ditentukan pengguna (default: '/admin')
61
59
  router.use(config.adminRoutePrefix, adminRouter);
62
-
63
- // adminRouter.get('/', newsController.adminList.bind(newsController));
64
- // adminRouter.post('/create', newsController.createPost.bind(newsController));
65
- // // ... Tambahkan route admin lain (/edit/:id, /delete/:id)
66
-
67
- // router.use(config.adminRoutePrefix, adminRouter);
68
-
69
60
 
70
61
  // 4. Route API (Untuk data trending)
71
62
  router.get('/api/trending', statController.getTrendingApi.bind(statController));
72
63
 
73
- // Router telah siap di file index.js
74
64
  };
@@ -6,7 +6,6 @@ class NewsService {
6
6
 
7
7
  // --- Operasi READ (Membaca) ---
8
8
  async getAllPosts(page = 1, limit = 10, status = 'PUBLISHED') {
9
- // Logika untuk mengambil daftar berita dengan pagination
10
9
  const offset = (page - 1) * limit;
11
10
  return this.News.findAndCountAll({
12
11
  where: { status },
@@ -17,13 +16,12 @@ class NewsService {
17
16
  }
18
17
 
19
18
  async getPostBySlug(slug) {
20
- // Mengambil berita beserta semua blok kontennya
21
19
  return this.News.findOne({
22
20
  where: { slug, status: 'PUBLISHED' },
23
21
  include: [{
24
22
  model: this.ContentNews,
25
23
  as: 'blocks',
26
- order: [['order', 'ASC']] // Pastikan konten diurutkan
24
+ order: [['order', 'ASC']]
27
25
  }]
28
26
  });
29
27
  }
@@ -44,10 +42,36 @@ class NewsService {
44
42
  });
45
43
  }
46
44
 
47
- // --- Operasi DELETE ---
45
+ async updatePost(id, newsData, contentBlocks) {
46
+ const existingNews = await this.News.findByPk(id);
47
+ if (!existingNews) {
48
+ return null;
49
+ }
50
+
51
+ return this.News.sequelize.transaction(async (t) => {
52
+ await existingNews.update(newsData, { transaction: t });
53
+
54
+ await this.ContentNews.destroy({
55
+ where: { newsId: id },
56
+ transaction: t
57
+ });
58
+
59
+ const blocks = contentBlocks.map((block, index) => ({
60
+ ...block,
61
+ newsId: id,
62
+ order: index + 1
63
+ }));
64
+
65
+ await this.ContentNews.bulkCreate(blocks, { transaction: t });
66
+
67
+ return this.News.findByPk(id, {
68
+ include: [{ model: this.ContentNews, as: 'blocks', order: [['order', 'ASC']] }],
69
+ transaction: t
70
+ });
71
+ });
72
+ }
73
+
48
74
  async deletePost(id) {
49
- // Karena kita menggunakan onDelete: 'CASCADE' di Models,
50
- // menghapus News akan otomatis menghapus ContentNews dan VisitorLog
51
75
  return this.News.destroy({ where: { id } });
52
76
  }
53
77
  }
@@ -1,5 +1,3 @@
1
- // services/StatService.js
2
-
3
1
  const { Op, literal } = require('sequelize');
4
2
 
5
3
  class StatService {
@@ -9,7 +7,6 @@ class StatService {
9
7
  }
10
8
 
11
9
  async trackVisit(newsId, sessionId) {
12
- // Mencoba membuat entri unik (newsId, sessionId). Jika sudah ada, Sequelize akan melempar error unik.
13
10
  try {
14
11
  await this.VisitorLog.create({
15
12
  newsId: newsId,
@@ -18,8 +15,6 @@ class StatService {
18
15
  });
19
16
  return true;
20
17
  } catch (error) {
21
- // Ini biasanya terjadi karena pelanggaran unique constraint (pengunjung yang sama mengklik lagi)
22
- // Di sini kita bisa mengabaikan error atau logging.
23
18
  return false;
24
19
  }
25
20
  }
@@ -27,26 +22,24 @@ class StatService {
27
22
  async getTrendingPosts(timeframeHours = 24, limit = 10) {
28
23
  const cutOffTime = new Date(new Date() - timeframeHours * 60 * 60 * 1000);
29
24
 
30
- // Menggunakan Sequelize untuk melakukan GROUP BY dan COUNT
31
25
  const trendingLogs = await this.VisitorLog.findAll({
32
26
  attributes: [
33
27
  'newsId',
34
- [literal('COUNT(DISTINCT sessionId)'), 'uniqueVisits'] // Hitung sesi unik
28
+ [literal('COUNT(DISTINCT sessionId)'), 'uniqueVisits']
35
29
  ],
36
30
  where: {
37
- visitedAt: { [Op.gte]: cutOffTime } // Filter berdasarkan waktu
31
+ visitedAt: { [Op.gte]: cutOffTime }
38
32
  },
39
33
  group: ['newsId'],
40
- order: [[literal('uniqueVisits'), 'DESC']], // Urutkan berdasarkan kunjungan terbanyak
34
+ order: [[literal('uniqueVisits'), 'DESC']],
41
35
  limit: limit
42
36
  });
43
37
 
44
38
  const newsIds = trendingLogs.map(log => log.newsId);
45
39
 
46
- // Ambil data berita lengkapnya
40
+
47
41
  return this.News.findAll({
48
42
  where: { id: newsIds, status: 'PUBLISHED' },
49
- // Tambahkan order agar urutan trending tetap terjaga
50
43
  order: [[literal(`FIELD(id, ${newsIds.join(',')})`)]], // MySQL specific ordering
51
44
  });
52
45
  }
package/services/index.js CHANGED
@@ -1,5 +1,3 @@
1
- // services/index.js
2
-
3
1
  const NewsService = require('./NewsService');
4
2
  const StatService = require('./StatService');
5
3