zyket 1.0.30 → 1.0.32

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/index.js CHANGED
@@ -7,6 +7,9 @@ const { Handler, Guard } = require("./src/services/socketio");
7
7
  const Schedule = require("./src/services/scheduler/Schedule");
8
8
  const Event = require("./src/services/events/Event");
9
9
  const Worker = require("./src/services/bullmq/Worker");
10
+ const BullBoardExtension = require("./src/extensions/bullboard");
11
+ const Extension = require("./src/extensions/Extension");
12
+ const InteractiveStorageExtension = require("./src/extensions/interactive-storage");
10
13
 
11
14
 
12
15
  module.exports = {
@@ -16,5 +19,7 @@ module.exports = {
16
19
  Handler, Guard,
17
20
  Schedule, Event,
18
21
  Worker,
19
- EnvManager
22
+ EnvManager,
23
+ BullBoardExtension, InteractiveStorageExtension,
24
+ Extension
20
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zyket",
3
- "version": "1.0.30",
3
+ "version": "1.0.32",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -13,6 +13,7 @@
13
13
  "license": "ISC",
14
14
  "description": "",
15
15
  "dependencies": {
16
+ "@bull-board/express": "^6.14.2",
16
17
  "bullmq": "^5.63.0",
17
18
  "colors": "^1.4.0",
18
19
  "cors": "^2.8.5",
@@ -21,6 +22,7 @@
21
22
  "fast-glob": "^3.3.3",
22
23
  "mariadb": "^3.4.5",
23
24
  "minio": "^8.0.6",
25
+ "multer": "^2.0.2",
24
26
  "node-cron": "^4.2.1",
25
27
  "node-dependency-injection": "^3.2.4",
26
28
  "prompts": "^2.4.2",
@@ -0,0 +1,11 @@
1
+ module.exports = class Extension {
2
+ name;
3
+
4
+ constructor(_Name) {
5
+ this.name = _Name;
6
+ }
7
+
8
+ load(container) {
9
+ throw new Error("Load method not implemented");
10
+ }
11
+ }
@@ -0,0 +1,28 @@
1
+ const Extension = require('../Extension');
2
+ const { createBullBoard } = require('@bull-board/api')
3
+ const { BullMQAdapter } = require('@bull-board/api/bullMQAdapter')
4
+ const { ExpressAdapter } = require('@bull-board/express')
5
+
6
+ module.exports = class BullBoardExtension extends Extension {
7
+ path;
8
+
9
+ constructor({ path = '/bullboard' } = {}) {
10
+ super("BullBoardExtension");
11
+ this.path = path || '/bullboard';
12
+ }
13
+
14
+ load(container) {
15
+ if (!container.get('bullmq')) return container.get('logger').warn('BullBoardExtension: bullmq service not found, skipping BullBoard setup');
16
+ const bull = container.get('bullmq')
17
+ const serverAdapter = new ExpressAdapter()
18
+ serverAdapter.setBasePath(this.path)
19
+
20
+ createBullBoard({
21
+ queues: Object.values(bull.queues).map(queue => new BullMQAdapter(queue)),
22
+ serverAdapter
23
+ })
24
+
25
+ const app = container.get('express').app()
26
+ app.use(this.path, serverAdapter.getRouter())
27
+ }
28
+ }
@@ -0,0 +1,161 @@
1
+ const Extension = require('../Extension');
2
+ const multer = require('multer');
3
+
4
+ // Import route handlers
5
+ const uploadHandler = require('./routes/upload');
6
+ const browseHandler = require('./routes/browse');
7
+ const downloadHandler = require('./routes/download');
8
+ const infoHandler = require('./routes/info');
9
+ const deleteHandler = require('./routes/delete');
10
+ const createFolderHandler = require('./routes/create-folder');
11
+ const deleteFolderHandler = require('./routes/delete-folder');
12
+
13
+ module.exports = class InteractiveStorageExtension extends Extension {
14
+ path;
15
+ bucketName;
16
+ maxFileSize;
17
+ middlewares;
18
+
19
+ constructor({ path = '/storage', bucketName = 'dropbox', maxFileSize = 100 * 1024 * 1024, middlewares = [] } = {}) {
20
+ super("InteractiveStorageExtension");
21
+ this.path = path || '/storage';
22
+ this.bucketName = bucketName;
23
+ this.maxFileSize = maxFileSize; // Default 100MB
24
+ this.middlewares = middlewares || [];
25
+ }
26
+
27
+ load(container) {
28
+ if (!container.get('s3')) return container.get('logger').warn('InteractiveStorageExtension: s3 service not found, skipping InteractiveStorage setup');
29
+ if (!container.get('express')) return container.get('logger').warn('InteractiveStorageExtension: express service not found, skipping InteractiveStorage setup');
30
+
31
+ const app = container.get('express').app();
32
+ const s3 = container.get('s3');
33
+ const logger = container.get('logger');
34
+
35
+ // Ensure bucket exists
36
+ this.#ensureBucket(s3, logger);
37
+
38
+ // Configure multer for file uploads (memory storage)
39
+ const upload = multer({
40
+ storage: multer.memoryStorage(),
41
+ limits: {
42
+ fileSize: this.maxFileSize
43
+ }
44
+ });
45
+
46
+ // Bind helper methods
47
+ const normalizePath = this.#normalizePath.bind(this);
48
+ const listFiles = this.#listFiles.bind(this);
49
+ const listFilesAndFolders = this.#listFilesAndFolders.bind(this);
50
+ const getFileStat = this.#getFileStat.bind(this);
51
+
52
+ // Register routes
53
+ app.post(`${this.path}/upload`, upload.array('files'), uploadHandler(s3, this.bucketName, logger, normalizePath));
54
+ app.get(`${this.path}/browse`, browseHandler(s3, this.bucketName, logger, normalizePath, listFilesAndFolders));
55
+ app.get(`${this.path}/download/:fileName`, downloadHandler(s3, this.bucketName, logger, getFileStat));
56
+ app.get(`${this.path}/info/:fileName`, infoHandler(s3, this.bucketName, logger, getFileStat));
57
+ app.delete(`${this.path}/delete`, deleteHandler(s3, this.bucketName, logger));
58
+ app.post(`${this.path}/create-folder`, createFolderHandler(s3, this.bucketName, logger, normalizePath));
59
+ app.delete(`${this.path}/delete-folder`, deleteFolderHandler(s3, this.bucketName, logger, normalizePath, listFiles));
60
+
61
+ logger.info(`InteractiveStorage extension loaded at ${this.path}`);
62
+ }
63
+
64
+ async #ensureBucket(s3, logger) {
65
+ try {
66
+ const buckets = await s3.listBuckets();
67
+ const bucketExists = buckets.some(bucket => bucket.name === this.bucketName);
68
+
69
+ if (!bucketExists) {
70
+ await s3.createBucket(this.bucketName);
71
+ logger.info(`Created S3 bucket: ${this.bucketName}`);
72
+ }
73
+ } catch (error) {
74
+ logger.error(`Error ensuring bucket exists: ${error.message}`);
75
+ }
76
+ }
77
+
78
+ async #listFiles(s3, prefix = '') {
79
+ return new Promise((resolve, reject) => {
80
+ const files = [];
81
+ const stream = s3.client.listObjectsV2(this.bucketName, prefix, true);
82
+
83
+ stream.on('data', (obj) => {
84
+ files.push({
85
+ name: obj.name,
86
+ size: obj.size,
87
+ lastModified: obj.lastModified,
88
+ etag: obj.etag
89
+ });
90
+ });
91
+
92
+ stream.on('end', () => resolve(files));
93
+ stream.on('error', reject);
94
+ });
95
+ }
96
+
97
+ async #listFilesAndFolders(s3, prefix = '') {
98
+ return new Promise((resolve, reject) => {
99
+ const items = [];
100
+ const folders = new Set();
101
+
102
+ const stream = s3.client.listObjectsV2(this.bucketName, prefix, false);
103
+
104
+ stream.on('data', (obj) => {
105
+ if (obj.prefix) {
106
+ // This is a folder
107
+ const folderName = obj.prefix.slice(prefix.length).replace(/\/$/, '');
108
+ if (folderName && !folderName.includes('/')) {
109
+ folders.add(folderName);
110
+ }
111
+ } else if (obj.name) {
112
+ // This is a file
113
+ const fileName = obj.name.slice(prefix.length);
114
+
115
+ // Skip folder markers and files in subfolders
116
+ if (fileName === '.folder' || fileName.includes('/')) {
117
+ return;
118
+ }
119
+
120
+ items.push({
121
+ type: 'file',
122
+ name: fileName,
123
+ fullPath: obj.name,
124
+ size: obj.size,
125
+ lastModified: obj.lastModified,
126
+ etag: obj.etag
127
+ });
128
+ }
129
+ });
130
+
131
+ stream.on('end', () => {
132
+ // Add folders to items
133
+ folders.forEach(folder => {
134
+ items.unshift({
135
+ type: 'folder',
136
+ name: folder,
137
+ fullPath: prefix + folder
138
+ });
139
+ });
140
+
141
+ resolve(items);
142
+ });
143
+
144
+ stream.on('error', reject);
145
+ });
146
+ }
147
+
148
+ #normalizePath(path) {
149
+ // Remove leading/trailing slashes and normalize
150
+ return path.replace(/^\/+|\/+$/g, '').replace(/\/+/g, '/');
151
+ }
152
+
153
+ async #getFileStat(s3, fileName) {
154
+ return new Promise((resolve, reject) => {
155
+ s3.client.statObject(this.bucketName, fileName, (err, stat) => {
156
+ if (err) return reject(err);
157
+ resolve(stat);
158
+ });
159
+ });
160
+ }
161
+ }
@@ -0,0 +1,18 @@
1
+ module.exports = (s3, bucketName, logger, normalizePath, listFilesAndFolders) => async (req, res) => {
2
+ try {
3
+ const folder = req.query.folder || '';
4
+ const prefix = folder ? normalizePath(folder) + '/' : '';
5
+
6
+ const items = await listFilesAndFolders(s3, prefix);
7
+
8
+ res.json({
9
+ success: true,
10
+ bucket: bucketName,
11
+ currentPath: folder,
12
+ items: items
13
+ });
14
+ } catch (error) {
15
+ logger.error(`Error browsing files: ${error.message}`);
16
+ res.status(500).json({ success: false, message: error.message });
17
+ }
18
+ };
@@ -0,0 +1,26 @@
1
+ module.exports = (s3, bucketName, logger, normalizePath) => async (req, res) => {
2
+ try {
3
+ const { folderPath } = req.body;
4
+
5
+ if (!folderPath) {
6
+ return res.status(400).json({ success: false, message: 'Folder path is required' });
7
+ }
8
+
9
+ const normalizedPath = normalizePath(folderPath);
10
+ const folderMarker = `${normalizedPath}/.folder`;
11
+
12
+ // Create an empty marker file to represent the folder
13
+ await s3.saveFile(bucketName, folderMarker, Buffer.from(''), 'text/plain');
14
+
15
+ logger.info(`Created folder: ${normalizedPath}`);
16
+
17
+ res.json({
18
+ success: true,
19
+ message: 'Folder created successfully',
20
+ folderPath: normalizedPath
21
+ });
22
+ } catch (error) {
23
+ logger.error(`Error creating folder: ${error.message}`);
24
+ res.status(500).json({ success: false, message: error.message });
25
+ }
26
+ };
@@ -0,0 +1,44 @@
1
+ module.exports = (s3, bucketName, logger, normalizePath, listFiles) => async (req, res) => {
2
+ try {
3
+ const { folderPath } = req.body;
4
+
5
+ if (!folderPath) {
6
+ return res.status(400).json({ success: false, message: 'Folder path is required' });
7
+ }
8
+
9
+ const normalizedPath = normalizePath(folderPath);
10
+ const prefix = `${normalizedPath}/`;
11
+
12
+ // List all files in the folder
13
+ const files = await listFiles(s3, prefix);
14
+
15
+ if (files.length === 0) {
16
+ return res.json({
17
+ success: true,
18
+ message: 'Folder is empty or does not exist',
19
+ deletedCount: 0
20
+ });
21
+ }
22
+
23
+ // Delete all files
24
+ const deletePromises = files.map(file =>
25
+ s3.removeFile(bucketName, file.name).catch(err => ({ error: err.message, fileName: file.name }))
26
+ );
27
+
28
+ const results = await Promise.all(deletePromises);
29
+ const errors = results.filter(r => r && r.error);
30
+ const successCount = results.length - errors.length;
31
+
32
+ logger.info(`Deleted folder ${normalizedPath} with ${successCount} files`);
33
+
34
+ res.json({
35
+ success: errors.length === 0,
36
+ message: `Deleted folder and ${successCount} files`,
37
+ deletedCount: successCount,
38
+ errors: errors.length > 0 ? errors : undefined
39
+ });
40
+ } catch (error) {
41
+ logger.error(`Error deleting folder: ${error.message}`);
42
+ res.status(500).json({ success: false, message: error.message });
43
+ }
44
+ };
@@ -0,0 +1,32 @@
1
+ module.exports = (s3, bucketName, logger) => async (req, res) => {
2
+ try {
3
+ const { fileName, fileNames } = req.body;
4
+
5
+ // Support both single file and multiple files
6
+ const filesToDelete = fileNames || (fileName ? [fileName] : []);
7
+
8
+ if (!Array.isArray(filesToDelete) || filesToDelete.length === 0) {
9
+ return res.status(400).json({ success: false, message: 'fileName or fileNames array is required' });
10
+ }
11
+
12
+ const deletePromises = filesToDelete.map(file =>
13
+ s3.removeFile(bucketName, file).catch(err => ({ error: err.message, fileName: file }))
14
+ );
15
+
16
+ const results = await Promise.all(deletePromises);
17
+ const errors = results.filter(r => r && r.error);
18
+ const successCount = results.length - errors.length;
19
+
20
+ logger.info(`Deleted ${successCount} file(s) from S3 dropbox`);
21
+
22
+ res.json({
23
+ success: errors.length === 0,
24
+ message: `Deleted ${successCount} of ${filesToDelete.length} file(s)`,
25
+ deletedCount: successCount,
26
+ errors: errors.length > 0 ? errors : undefined
27
+ });
28
+ } catch (error) {
29
+ logger.error(`Error deleting files: ${error.message}`);
30
+ res.status(500).json({ success: false, message: error.message });
31
+ }
32
+ };
@@ -0,0 +1,30 @@
1
+ module.exports = (s3, bucketName, logger, getFileStat) => async (req, res) => {
2
+ try {
3
+ const { fileName } = req.params;
4
+
5
+ // Get file info first
6
+ const stat = await getFileStat(s3, fileName);
7
+
8
+ // Set response headers
9
+ res.setHeader('Content-Type', stat.metaData?.['content-type'] || 'application/octet-stream');
10
+ res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
11
+ res.setHeader('Content-Length', stat.size);
12
+
13
+ // Stream the file
14
+ await new Promise((resolve, reject) => {
15
+ s3.client.getObject(bucketName, fileName, (err, stream) => {
16
+ if (err) return reject(err);
17
+ stream.pipe(res);
18
+ stream.on('end', resolve);
19
+ stream.on('error', reject);
20
+ });
21
+ });
22
+
23
+ logger.info(`Downloaded file: ${fileName}`);
24
+ } catch (error) {
25
+ logger.error(`Error downloading file: ${error.message}`);
26
+ if (!res.headersSent) {
27
+ res.status(404).json({ success: false, message: 'File not found' });
28
+ }
29
+ }
30
+ };
@@ -0,0 +1,20 @@
1
+ module.exports = (s3, bucketName, logger, getFileStat) => async (req, res) => {
2
+ try {
3
+ const { fileName } = req.params;
4
+ const stat = await getFileStat(s3, fileName);
5
+
6
+ res.json({
7
+ success: true,
8
+ file: {
9
+ name: fileName,
10
+ size: stat.size,
11
+ lastModified: stat.lastModified,
12
+ etag: stat.etag,
13
+ contentType: stat.metaData?.['content-type']
14
+ }
15
+ });
16
+ } catch (error) {
17
+ logger.error(`Error getting file info: ${error.message}`);
18
+ res.status(404).json({ success: false, message: 'File not found' });
19
+ }
20
+ };
@@ -0,0 +1,34 @@
1
+ module.exports = (s3, bucketName, logger, normalizePath) => async (req, res) => {
2
+ try {
3
+ if (!req.files || req.files.length === 0) {
4
+ return res.status(400).json({ success: false, message: 'No files uploaded' });
5
+ }
6
+
7
+ const folder = req.body.folder || '';
8
+ const folderPath = folder ? normalizePath(folder) + '/' : '';
9
+
10
+ const uploadPromises = req.files.map(async (file) => {
11
+ const fileName = `${folderPath}${Date.now()}-${file.originalname}`;
12
+ await s3.saveFile(bucketName, fileName, file.buffer, file.mimetype);
13
+ return {
14
+ originalName: file.originalname,
15
+ fileName: fileName,
16
+ folder: folder,
17
+ size: file.size,
18
+ mimeType: file.mimetype
19
+ };
20
+ });
21
+
22
+ const uploadedFiles = await Promise.all(uploadPromises);
23
+ logger.info(`Uploaded ${uploadedFiles.length} file(s) to S3 dropbox`);
24
+
25
+ res.json({
26
+ success: true,
27
+ message: 'Files uploaded successfully',
28
+ files: uploadedFiles
29
+ });
30
+ } catch (error) {
31
+ logger.error(`Error uploading files: ${error.message}`);
32
+ res.status(500).json({ success: false, message: error.message });
33
+ }
34
+ };
@@ -3,18 +3,22 @@ const EnvManager = require("../utils/EnvManager");
3
3
  const fs = require("fs");
4
4
  const path = require("path");
5
5
  const HTTPServer = require("./HTTPServer");
6
+ const Extension = require("../extensions/Extension");
6
7
 
7
8
  module.exports = class Kernel {
8
9
  container;
9
10
  #services;
10
11
  #onSocketConnection;
11
12
  #httpServer;
13
+ #extensions = [];
12
14
 
13
15
  constructor({
14
- services = []
16
+ services = [],
17
+ extensions = [],
15
18
  } = { }) {
16
19
  this.container = new ContainerBuilder();
17
20
  this.#services = services;
21
+ this.#extensions = extensions;
18
22
 
19
23
  // create src folder if not exists
20
24
  if (!fs.existsSync(path.join(process.cwd(), "src"))) {
@@ -41,6 +45,16 @@ module.exports = class Kernel {
41
45
  });
42
46
  }
43
47
 
48
+ for (const extension of this.#extensions) {
49
+ if (!(extension instanceof Extension)) {
50
+ throw new Error(`Extension ${extension.name} is not an instance of Extension class`);
51
+ }
52
+ this.container.get('logger').debug(`Loading extension ${extension.name}`);
53
+ await extension.load(this.container);
54
+ }
55
+
56
+
57
+
44
58
  return this;
45
59
  }
46
60
 
@@ -58,4 +72,8 @@ module.exports = class Kernel {
58
72
 
59
73
  this.container.compile();
60
74
  }
75
+
76
+ async #loadExtensions() {
77
+ // Load extensions if any
78
+ }
61
79
  }
@@ -57,6 +57,24 @@ module.exports = class Database extends Service {
57
57
  }
58
58
  }
59
59
 
60
+ async loadModel(model) {
61
+ if (typeof model !== 'function') {
62
+ this.#container.get('logger').error(`Model ${model} is not a function`);
63
+ throw new Error(`Model ${model} is not a function`);
64
+ }
65
+
66
+ const modelInstance = model({sequelize: this.sequelize, container: this.#container, Sequelize});
67
+ if(this.models[modelInstance.name]) {
68
+ this.#container.get('logger').warn(`Model ${modelInstance.name} is already loaded, cannot load it again`);
69
+ throw new Error(`Model ${modelInstance.name} is already loaded, cannot load it again`);
70
+ }
71
+ this.models[modelInstance.name] = modelInstance;
72
+
73
+ if (modelInstance.associate) {
74
+ modelInstance.associate(this.models);
75
+ }
76
+ }
77
+
60
78
  #createModelsFolder() {
61
79
  const path = 'src/models';
62
80
  if (!fs.existsSync(path)) fs.mkdirSync(path);