news-cms-module 0.0.1

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,34 @@
1
+ const path = require('path');
2
+
3
+ // Konfigurasi Default untuk Package Anda
4
+ const defaultConfig = {
5
+ // Pengaturan Router
6
+ adminRoutePrefix: '/admin', // Prefix default untuk route CRUD admin
7
+ publicRoutePrefix: '/', // Prefix default untuk route publik
8
+
9
+ // Pengaturan Views
10
+ viewEngine: 'ejs', // Template engine default
11
+ baseLayout: 'layout', // Nama file layout utama (jika digunakan)
12
+
13
+ // Pengaturan Assets (CSS/JS)
14
+ assetsUrlPrefix: '/nc-assets', // Prefix default untuk URL aset statis
15
+ assetsPath: path.join(__dirname, '..', 'assets'), // Path fisik default ke folder assets
16
+
17
+ // Pengaturan Pelacakan (Tracking)
18
+ trendingTimeframeHours: 24, // Jendela waktu default untuk menghitung trending (24 jam)
19
+ maxTrendingPosts: 10, // Jumlah maksimum berita trending yang dikembalikan
20
+ };
21
+
22
+ /**
23
+ * Menggabungkan konfigurasi default package dengan konfigurasi kustom dari pengguna.
24
+ * @param {object} userConfig - Konfigurasi yang disediakan oleh aplikasi yang menginstal package.
25
+ * @returns {object} Objek konfigurasi akhir yang lengkap.
26
+ */
27
+ module.exports = (userConfig = {}) => {
28
+ // Menggunakan penyebaran (spread operator) untuk menggabungkan objek.
29
+ // userConfig akan menimpa (overwrite) defaultConfig jika ada properti yang sama.
30
+ return {
31
+ ...defaultConfig,
32
+ ...userConfig
33
+ };
34
+ };
@@ -0,0 +1,102 @@
1
+ class NewsController {
2
+ constructor(newsService, statService, config) {
3
+ this.newsService = newsService;
4
+ this.statService = statService;
5
+ this.config = config; // Digunakan untuk passing config ke view (e.g. baseUrl)
6
+ }
7
+
8
+ // --- Route Publik ---
9
+ async listPublic(req, res) {
10
+ try {
11
+ const { rows: posts, count: total } = await this.newsService.getAllPosts(req.query.page);
12
+
13
+ res.status(200).json({
14
+ success: true,
15
+ data: posts,
16
+ total: total,
17
+ page: req.query.page
18
+ })
19
+
20
+ // res.render('list', { posts, total, baseUrl: req.baseUrl });
21
+ } catch (error) {
22
+ res.status(500).json({
23
+ succses: false,
24
+ error: 'Error loading news list.'
25
+ });
26
+ }
27
+ }
28
+
29
+ async getDetail(req, res) {
30
+ try {
31
+ const post = await this.newsService.getPostBySlug(req.params.slug);
32
+ if (!post) {
33
+ return res.status(404).json({
34
+ succses: false,
35
+ error: 'news tidak ditemukan'
36
+ });
37
+ }
38
+
39
+ res.status(200).json({
40
+ success: true,
41
+ data: post
42
+ })
43
+
44
+ // Render detail view
45
+ // res.render('detail', { post, baseUrl: req.baseUrl });
46
+ } catch (error) {
47
+ // res.status(500).send('Error loading post detail.');
48
+ res.status(500).json({
49
+ succses: false,
50
+ error: 'gagal melihat news'
51
+ });
52
+ }
53
+ }
54
+
55
+ // --- Route Admin (CRUD) ---
56
+ async adminList(req, res) {
57
+ // Logic ambil data dengan status DRAFT atau PUBLISHED untuk admin
58
+ const { rows: posts } = await this.newsService.getAllPosts(req.query.page, 10, ['DRAFT', 'PUBLISHED']);
59
+
60
+ res.status(200).json({
61
+ success: true,
62
+ data: posts
63
+ })
64
+
65
+ //render
66
+ // res.render('admin/list', { posts, baseUrl: req.baseUrl });
67
+ }
68
+
69
+ async createPost(req, res) {
70
+ try {
71
+ // Asumsi req.body.content adalah array of blocks [{blockType, contentValue}, ...]
72
+ const { title, summary, authorId, status, contentBlocks } = req.body;
73
+
74
+ // Slug harus dibuat sebelum disimpan
75
+ const slug = title.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, '');
76
+
77
+ const newNews = await this.newsService.createPost(
78
+ { title, slug, summary, authorId, status: status || 'DRAFT' },
79
+ contentBlocks
80
+ );
81
+
82
+ res.status(201).json({
83
+ success: true,
84
+ data: newNews
85
+ })
86
+
87
+ // Redirect ke halaman daftar admin atau detail admin
88
+ // res.redirect(this.config.adminRoutePrefix + '/');
89
+ } catch (error) {
90
+ console.error(error);
91
+ // res.status(500).send('Gagal membuat berita.');
92
+ res.status(500).json({
93
+ succses: false,
94
+ error: 'gagal membuat news'
95
+ });
96
+ }
97
+ }
98
+
99
+ // ... Tambahkan updatePost, deletePost, dll.
100
+ }
101
+
102
+ module.exports = NewsController;
@@ -0,0 +1,36 @@
1
+ // controllers/StatController.js
2
+
3
+ class StatController {
4
+ constructor(statService) {
5
+ this.statService = statService;
6
+ }
7
+
8
+ // Middleware untuk pelacakan kunjungan
9
+ async trackVisitMiddleware(req, res, next) {
10
+ const { slug } = req.params;
11
+
12
+ // Ambil post ID (idealnya dilakukan oleh service sebelum dipanggil)
13
+ // Untuk demo, kita asumsikan kita punya newsId dari service atau middleware sebelumnya.
14
+ // **REAL-WORLD:** Anda harus mencari newsId berdasarkan slug di sini atau di service.
15
+ const newsId = 1; // Contoh: Asumsi newsId ditemukan
16
+
17
+ const sessionId = req.sessionID || req.ip; // Gunakan sesi atau IP untuk unique ID
18
+
19
+ if (newsId) {
20
+ await this.statService.trackVisit(newsId, sessionId);
21
+ }
22
+
23
+ next(); // Lanjutkan ke controller NewsController.getDetail
24
+ }
25
+
26
+ async getTrendingApi(req, res) {
27
+ try {
28
+ const posts = await this.statService.getTrendingPosts();
29
+ res.json(posts);
30
+ } catch (error) {
31
+ res.status(500).json({ error: 'Failed to retrieve trending data.' });
32
+ }
33
+ }
34
+ }
35
+
36
+ module.exports = StatController;
package/index.js ADDED
@@ -0,0 +1,73 @@
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
+ /**
7
+ * Fungsi utama package yang di-export.
8
+ * @param {object} dbConfig - Konfigurasi koneksi database dari aplikasi pengguna.
9
+ * @param {object} options - Opsi tambahan (misalnya, nama folder views pengguna).
10
+ * @returns {express.Router} Router Express yang sudah terkonfigurasi.
11
+ */
12
+ // index.js (Revisi Akhir)
13
+
14
+ const express = require('express');
15
+ // Pastikan Anda telah mendefinisikan dan mengimpor fungsi-fungsi ini
16
+ const initModels = require('./models');
17
+ const initServices = require('./services');
18
+ const setupRoutes = require('./routes');
19
+ const mergeConfig = require('./config'); // Gunakan fungsi mergeConfig yang sudah kita buat
20
+
21
+ /**
22
+ * Fungsi utama package yang di-export.
23
+ * @param {object} dbConfig - Konfigurasi koneksi database dari aplikasi pengguna.
24
+ * @param {object} userOptions - Opsi tambahan (misalnya, autoMigrate) dari pengguna.
25
+ * @returns {express.Router} Router Express yang sudah terkonfigurasi.
26
+ */
27
+ module.exports = async (dbConfig, userOptions = {}) => { // <<< KUNCI 1: Jadikan ASYNC
28
+
29
+ // 1. Validasi dan Konfigurasi
30
+ if (!dbConfig || !dbConfig.database) {
31
+ throw new Error('News module requires database configuration (dbConfig).');
32
+ }
33
+
34
+ // Gabungkan konfigurasi database dan opsi pengguna (walaupun di sini kita hanya menggunakan dbConfig)
35
+ const finalConfig = mergeConfig({
36
+ ...userOptions,
37
+ db: dbConfig
38
+ });
39
+
40
+ // 2. Inisialisasi Koneksi Database dan Model
41
+ const db = initModels(finalConfig.db);
42
+
43
+ // 3. Sinkronisasi Tabel (Pembuatan Tabel Otomatis)
44
+ // Secara default, kita asumsikan sinkronisasi ON. Pengguna bisa menonaktifkannya dengan { autoMigrate: false }
45
+ if (finalConfig.autoMigrate !== false) {
46
+ try {
47
+ console.log('News module: Synchronizing database tables...');
48
+ // Memanggil syncTables yang didefinisikan di models/index.js
49
+ await db.syncTables({ alter: true });
50
+ console.log('News module: Synchronization complete.');
51
+ } catch (error) {
52
+ console.error('ERROR: News module failed to synchronize database tables.', error);
53
+ // Anda bisa memilih untuk melempar error agar server tidak berjalan tanpa tabel
54
+ throw error;
55
+ }
56
+ }
57
+
58
+ // 4. Inisialisasi Services (Injeksi Dependencies)
59
+ const services = initServices(db);
60
+
61
+ // 5. Setup Router Express
62
+ const router = express.Router();
63
+
64
+ // Middleware dasar untuk parsing body (penting untuk CRUD)
65
+ router.use(express.json());
66
+ router.use(express.urlencoded({ extended: true }));
67
+
68
+ // 6. Setup Router dan Controllers
69
+ // Route diatur, Controllers menggunakan Services dan finalConfig
70
+ setupRoutes(router, services, finalConfig);
71
+
72
+ return router;
73
+ };
@@ -0,0 +1,46 @@
1
+ // models/ContentNews.js
2
+
3
+ const { DataTypes } = require('sequelize');
4
+
5
+ module.exports = (sequelize) => {
6
+ const ContentNews = sequelize.define('ContentNews', {
7
+ id: {
8
+ type: DataTypes.INTEGER,
9
+ primaryKey: true,
10
+ autoIncrement: true,
11
+ },
12
+ newsId: {
13
+ type: DataTypes.INTEGER,
14
+ allowNull: false,
15
+ },
16
+ order: {
17
+ type: DataTypes.INTEGER,
18
+ allowNull: false,
19
+ },
20
+ blockType: {
21
+ type: DataTypes.ENUM(
22
+ 'PARAGRAPH',
23
+ 'IMAGE',
24
+ 'VIDEO',
25
+ 'SUBHEADING'
26
+ ),
27
+ allowNull: false,
28
+ },
29
+ contentValue: {
30
+ type: DataTypes.TEXT,
31
+ allowNull: false,
32
+ },
33
+ caption: {
34
+ type: DataTypes.STRING(255),
35
+ allowNull: true,
36
+ }
37
+ }, {
38
+ tableName: 'content_news',
39
+ timestamps: false,
40
+ indexes: [
41
+ { fields: ['newsId', 'order'] }
42
+ ]
43
+ });
44
+
45
+ return ContentNews;
46
+ };
package/models/News.js ADDED
@@ -0,0 +1,48 @@
1
+ // models/News.js
2
+
3
+ const { DataTypes } = require('sequelize');
4
+
5
+ module.exports = (sequelize) => {
6
+ const News = sequelize.define('News', {
7
+ id: {
8
+ type: DataTypes.INTEGER,
9
+ primaryKey: true,
10
+ autoIncrement: true,
11
+ },
12
+ title: {
13
+ type: DataTypes.STRING(255),
14
+ allowNull: false,
15
+ },
16
+ slug: {
17
+ type: DataTypes.STRING(255),
18
+ allowNull: false,
19
+ unique: true,
20
+ },
21
+ authorName: {
22
+ type: DataTypes.STRING(50),
23
+ allowNull: false,
24
+ },
25
+ category: {
26
+ type: DataTypes.STRING(100),
27
+ allowNull: true,
28
+ },
29
+ imagePath: {
30
+ type: DataTypes.STRING(255),
31
+ allowNull: true,
32
+ },
33
+ status: {
34
+ type: DataTypes.ENUM('DRAFT', 'PUBLISHED', 'ARCHIVED'),
35
+ defaultValue: 'DRAFT',
36
+ allowNull: false,
37
+ },
38
+ publishedAt: {
39
+ type: DataTypes.DATE,
40
+ allowNull: true,
41
+ }
42
+ }, {
43
+ tableName: 'news',
44
+ timestamps: true,
45
+ });
46
+
47
+ return News;
48
+ };
@@ -0,0 +1,35 @@
1
+ // models/VisitorLog.js
2
+
3
+ const { DataTypes } = require('sequelize');
4
+
5
+ module.exports = (sequelize) => {
6
+ const VisitorLog = sequelize.define('VisitorLog', {
7
+ id: {
8
+ type: DataTypes.INTEGER,
9
+ primaryKey: true,
10
+ autoIncrement: true,
11
+ },
12
+ newsId: {
13
+ type: DataTypes.INTEGER,
14
+ allowNull: false,
15
+ },
16
+ sessionId: {
17
+ type: DataTypes.STRING(100),
18
+ allowNull: false,
19
+ },
20
+ visitedAt: {
21
+ type: DataTypes.DATE,
22
+ defaultValue: DataTypes.NOW,
23
+ allowNull: false,
24
+ }
25
+ }, {
26
+ tableName: 'visitor_logs',
27
+ timestamps: false,
28
+ indexes: [
29
+ { fields: ['visitedAt'] },
30
+ { unique: true, fields: ['newsId', 'sessionId'] }
31
+ ]
32
+ });
33
+
34
+ return VisitorLog;
35
+ };
@@ -0,0 +1,61 @@
1
+ // models/index.js
2
+
3
+ const { Sequelize } = require('sequelize');
4
+ const NewsModel = require('./News');
5
+ const ContentNewsModel = require('./ContentNews');
6
+ const VisitorLogModel = require('./VisitorLog');
7
+
8
+ module.exports = (config) => {
9
+ // Inisialisasi Sequelize dengan konfigurasi dari aplikasi pengguna
10
+ const sequelize = new Sequelize(config.database, config.username, config.password, {
11
+ host: config.host,
12
+ dialect: 'mysql',
13
+ logging: false, // Matikan logging SQL
14
+ });
15
+
16
+ const db = {};
17
+
18
+ db.News = NewsModel(sequelize);
19
+ db.ContentNews = ContentNewsModel(sequelize);
20
+ db.VisitorLog = VisitorLogModel(sequelize);
21
+
22
+
23
+ db.News.hasMany(db.ContentNews, {
24
+ foreignKey: 'newsId',
25
+ as: 'blocks',
26
+ onDelete: 'CASCADE'
27
+ });
28
+ db.ContentNews.belongsTo(db.News, {
29
+ foreignKey: 'newsId',
30
+ as: 'newsItem'
31
+ });
32
+
33
+
34
+ db.News.hasMany(db.VisitorLog, {
35
+ foreignKey: 'newsId',
36
+ as: 'visits',
37
+ onDelete: 'CASCADE'
38
+ });
39
+ db.VisitorLog.belongsTo(db.News, {
40
+ foreignKey: 'newsId',
41
+ as: 'newsItem'
42
+ });
43
+
44
+
45
+ // --- PENAMBAHAN KUNCI UNTUK SINKRONISASI ---
46
+ /**
47
+ * Metode untuk mensinkronisasi model dengan tabel database.
48
+ * @param {object} options - Opsi sinkronisasi Sequelize (e.g., { alter: true } atau { force: true }).
49
+ */
50
+ db.syncTables = async (options = { alter: true }) => {
51
+ // Kami menggunakan { alter: true } secara default, yang akan membuat tabel jika belum ada
52
+ // dan mencoba mengubah kolom yang ada agar sesuai dengan definisi model, tanpa menghapus data.
53
+ await sequelize.sync(options);
54
+ };
55
+ // ----------------------------------------------
56
+
57
+ db.sequelize = sequelize;
58
+ db.Sequelize = Sequelize;
59
+
60
+ return db;
61
+ };
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "news-cms-module",
3
+ "version": "0.0.1",
4
+ "main": "index.js",
5
+ "scripts": {
6
+ "test": "echo \"Error: no test specified\" && exit 1"
7
+ },
8
+ "keywords": [],
9
+ "author": "",
10
+ "license": "ISC",
11
+ "description": "Package CRUD Berita modular dengan statistik",
12
+ "dependencies": {
13
+ "dotenv": "^17.2.3",
14
+ "ejs": "^3.1.10",
15
+ "express": "^5.1.0",
16
+ "mysql2": "^3.15.1",
17
+ "path": "^0.12.7",
18
+ "sequelize": "^6.37.7"
19
+ }
20
+ }
@@ -0,0 +1,74 @@
1
+ // routes/index.js
2
+
3
+ const express = require('express');
4
+ const NewsController = require('../controllers/NewsController');
5
+ const StatController = require('../controllers/StatController');
6
+ const path = require('path');
7
+
8
+ /**
9
+ * Setup semua route untuk package.
10
+ * @param {express.Router} router - Router Express yang sudah ada.
11
+ * @param {object} services - Semua services yang telah diinisialisasi.
12
+ * @param {object} config - Konfigurasi package lengkap.
13
+ */
14
+ module.exports = (router, services, config) => {
15
+ // Inisialisasi Controllers dengan services dan config yang dibutuhkan
16
+ const newsController = new NewsController(services.news, services.stat, config);
17
+ const statController = new StatController(services.stat);
18
+
19
+ // 1. Ekspos Aset Statis (CSS)
20
+ // Misalnya, package diakses di /berita-kami, maka aset diakses di /berita-kami/nc-assets
21
+ // router.use(config.assetsUrlPrefix, express.static(config.assetsPath));
22
+
23
+ // 2. Route Publik (Front-end)
24
+ // Route ini menggunakan prefix yang ditentukan oleh pengguna (default: '/')
25
+ // router.get(config.publicRoutePrefix, newsController.listPublic.bind(newsController));
26
+
27
+ // 2. Route Publik (READ-ONLY API)
28
+ // Gunakan endpoint yang jelas: /list, /trending, /:slug
29
+ router.get(config.publicRoutePrefix + 'list', newsController.listPublic.bind(newsController));
30
+
31
+ // Route detail berita, termasuk tracking middleware
32
+ router.get(`${config.publicRoutePrefix}post/:slug`,
33
+ statController.trackVisitMiddleware.bind(statController),
34
+ newsController.getDetail.bind(newsController));
35
+
36
+ // Route detail berita (termasuk tracking middleware)
37
+ // PERHATIAN: Di controller, Anda harus memastikan baseUrl dikirim ke view
38
+ // router.get(`${config.publicRoutePrefix}:slug`, statController.trackVisitMiddleware.bind(statController), newsController.getDetail.bind(newsController));
39
+
40
+ // 3. Route Admin (CRUD)
41
+ // Route ini menggunakan prefix admin yang ditentukan oleh pengguna (default: '/admin')
42
+ const adminRouter = express.Router();
43
+ // Middleware otentikasi admin bisa ditambahkan di sini (user harus menyediakannya!)
44
+
45
+ // GET /admin/list (Membaca semua post, termasuk DRAFT)
46
+ adminRouter.get('/list', newsController.adminList.bind(newsController));
47
+
48
+ // GET /admin/:id (Membaca detail post untuk editing)
49
+ adminRouter.get('/:id', newsController.getDetail.bind(newsController));
50
+
51
+ // POST /admin/create (Membuat berita baru)
52
+ adminRouter.post('/create', newsController.createPost.bind(newsController));
53
+
54
+ // PUT/PATCH /admin/update/:id (Memperbarui berita)
55
+ // adminRouter.put('/update/:id', newsController.updatePost.bind(newsController));
56
+
57
+ // DELETE /admin/delete/:id (Menghapus berita)
58
+ // adminRouter.delete('/delete/:id', newsController.deletePost.bind(newsController));
59
+
60
+ // Pasang router admin ke prefix yang ditentukan pengguna (default: '/admin')
61
+ 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
+
70
+ // 4. Route API (Untuk data trending)
71
+ router.get('/api/trending', statController.getTrendingApi.bind(statController));
72
+
73
+ // Router telah siap di file index.js
74
+ };
@@ -0,0 +1,55 @@
1
+ class NewsService {
2
+ constructor(NewsModel, ContentNewsModel) {
3
+ this.News = NewsModel;
4
+ this.ContentNews = ContentNewsModel;
5
+ }
6
+
7
+ // --- Operasi READ (Membaca) ---
8
+ async getAllPosts(page = 1, limit = 10, status = 'PUBLISHED') {
9
+ // Logika untuk mengambil daftar berita dengan pagination
10
+ const offset = (page - 1) * limit;
11
+ return this.News.findAndCountAll({
12
+ where: { status },
13
+ limit: limit,
14
+ offset: offset,
15
+ order: [['publishedAt', 'DESC']],
16
+ });
17
+ }
18
+
19
+ async getPostBySlug(slug) {
20
+ // Mengambil berita beserta semua blok kontennya
21
+ return this.News.findOne({
22
+ where: { slug, status: 'PUBLISHED' },
23
+ include: [{
24
+ model: this.ContentNews,
25
+ as: 'blocks',
26
+ order: [['order', 'ASC']] // Pastikan konten diurutkan
27
+ }]
28
+ });
29
+ }
30
+
31
+ // --- Operasi CREATE & UPDATE ---
32
+ async createPost(newsData, contentBlocks) {
33
+ return this.News.sequelize.transaction(async (t) => {
34
+ const newsItem = await this.News.create(newsData, { transaction: t });
35
+
36
+ const blocks = contentBlocks.map((block, index) => ({
37
+ ...block,
38
+ newsId: newsItem.id,
39
+ order: index + 1
40
+ }));
41
+
42
+ await this.ContentNews.bulkCreate(blocks, { transaction: t });
43
+ return newsItem;
44
+ });
45
+ }
46
+
47
+ // --- Operasi DELETE ---
48
+ async deletePost(id) {
49
+ // Karena kita menggunakan onDelete: 'CASCADE' di Models,
50
+ // menghapus News akan otomatis menghapus ContentNews dan VisitorLog
51
+ return this.News.destroy({ where: { id } });
52
+ }
53
+ }
54
+
55
+ module.exports = NewsService;
@@ -0,0 +1,55 @@
1
+ // services/StatService.js
2
+
3
+ const { Op, literal } = require('sequelize');
4
+
5
+ class StatService {
6
+ constructor(VisitorLogModel, NewsModel) {
7
+ this.VisitorLog = VisitorLogModel;
8
+ this.News = NewsModel;
9
+ }
10
+
11
+ async trackVisit(newsId, sessionId) {
12
+ // Mencoba membuat entri unik (newsId, sessionId). Jika sudah ada, Sequelize akan melempar error unik.
13
+ try {
14
+ await this.VisitorLog.create({
15
+ newsId: newsId,
16
+ sessionId: sessionId,
17
+ visitedAt: new Date()
18
+ });
19
+ return true;
20
+ } 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
+ return false;
24
+ }
25
+ }
26
+
27
+ async getTrendingPosts(timeframeHours = 24, limit = 10) {
28
+ const cutOffTime = new Date(new Date() - timeframeHours * 60 * 60 * 1000);
29
+
30
+ // Menggunakan Sequelize untuk melakukan GROUP BY dan COUNT
31
+ const trendingLogs = await this.VisitorLog.findAll({
32
+ attributes: [
33
+ 'newsId',
34
+ [literal('COUNT(DISTINCT sessionId)'), 'uniqueVisits'] // Hitung sesi unik
35
+ ],
36
+ where: {
37
+ visitedAt: { [Op.gte]: cutOffTime } // Filter berdasarkan waktu
38
+ },
39
+ group: ['newsId'],
40
+ order: [[literal('uniqueVisits'), 'DESC']], // Urutkan berdasarkan kunjungan terbanyak
41
+ limit: limit
42
+ });
43
+
44
+ const newsIds = trendingLogs.map(log => log.newsId);
45
+
46
+ // Ambil data berita lengkapnya
47
+ return this.News.findAll({
48
+ where: { id: newsIds, status: 'PUBLISHED' },
49
+ // Tambahkan order agar urutan trending tetap terjaga
50
+ order: [[literal(`FIELD(id, ${newsIds.join(',')})`)]], // MySQL specific ordering
51
+ });
52
+ }
53
+ }
54
+
55
+ module.exports = StatService;
@@ -0,0 +1,17 @@
1
+ // services/index.js
2
+
3
+ const NewsService = require('./NewsService');
4
+ const StatService = require('./StatService');
5
+
6
+ /**
7
+ * Menginisialisasi semua service dan menyuntikkan model database.
8
+ * @param {object} db - Objek yang berisi semua model Sequelize yang terkoneksi (News, ContentNews, VisitorLog).
9
+ * @returns {object} Objek yang berisi semua service yang siap digunakan.
10
+ */
11
+ module.exports = (db) => {
12
+ return {
13
+ news: new NewsService(db.News, db.ContentNews), // NewsService butuh News dan ContentNews
14
+ stat: new StatService(db.VisitorLog, db.News), // StatService butuh VisitorLog dan News
15
+ db: db // Mungkin perlu mengakses Sequelize instance (opsional)
16
+ };
17
+ };
File without changes
File without changes
File without changes
File without changes
package/views/list.ejs ADDED
File without changes