zyket 1.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,38 @@
1
+ name: Publish Package to npmjs
2
+ on:
3
+ push:
4
+ branches:
5
+ - main
6
+ jobs:
7
+ build:
8
+ runs-on: ubuntu-latest
9
+ permissions:
10
+ contents: write
11
+ packages: write
12
+ attestations: write
13
+ id-token: write
14
+ steps:
15
+ - name: Checkout code
16
+ uses: actions/checkout@v2
17
+
18
+ - name: Setup Node.js
19
+ uses: actions/setup-node@v4
20
+ with:
21
+ node-version: '20.x'
22
+ registry-url: 'https://registry.npmjs.org'
23
+
24
+ - name: Install dependencies
25
+ run: npm install
26
+
27
+ - name: 'Bump version'
28
+ id: version-bump
29
+ uses: 'phips28/gh-action-bump-version@master'
30
+ env:
31
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
32
+ with:
33
+ commit-message: 'CI: bumps version to {{version}} [skip ci]'
34
+
35
+ - name: Publish to npm
36
+ run: npm publish
37
+ env:
38
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # ZYKET - Easy Socket.io Framework
2
+
3
+ Zyket is a framework inspired by Symfony.
package/index.js ADDED
@@ -0,0 +1,12 @@
1
+ const Kernel = require("./src/kernel");
2
+ const Service = require("./src/services/Service");
3
+ const { Handler, Middleware } = require("./src/services/socketio");
4
+ const EnvManager = require("./src/utils/EnvManager");
5
+
6
+ module.exports = {
7
+ Kernel,
8
+ Service,
9
+ Handler,
10
+ Middleware,
11
+ EnvManager
12
+ }
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "zyket",
3
+ "version": "1.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": "",
12
+ "dependencies": {
13
+ "colors": "^1.4.0",
14
+ "dotenv": "^16.4.7",
15
+ "fast-glob": "^3.3.3",
16
+ "mariadb": "^3.4.0",
17
+ "minio": "^8.0.5",
18
+ "node-dependency-injection": "^3.2.2",
19
+ "redis": "^4.7.0",
20
+ "sequelize": "^6.37.6",
21
+ "socket.io": "^4.8.1"
22
+ }
23
+ }
@@ -0,0 +1,4 @@
1
+ module.exports = class Middleware {
2
+ constructor() {
3
+ }
4
+ }
@@ -0,0 +1,52 @@
1
+ const {ContainerBuilder} = require("node-dependency-injection");
2
+ const EnvManager = require("../utils/EnvManager");
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ module.exports = class Kernel {
7
+ container;
8
+ #services;
9
+ #onSocketConnection;
10
+
11
+ constructor({
12
+ services = [],
13
+ onConnection = () => { },
14
+ } = { }) {
15
+ this.container = new ContainerBuilder();
16
+ this.#services = services;
17
+
18
+ // create src folder if not exists
19
+ if (!fs.existsSync(path.join(process.cwd(), "src"))) {
20
+ fs.mkdirSync(path.join(process.cwd(), "src"));
21
+ }
22
+ }
23
+
24
+ async boot(clearConsole = true, secretsPath = `${process.cwd()}/.env`) {
25
+ EnvManager.load(secretsPath, false);
26
+ if(clearConsole) process.stdout.write("\u001b[2J\u001b[0;0H");
27
+ const services = require("../services");
28
+
29
+ await this.#registerServices(services);
30
+ await this.#registerServices(this.#services);
31
+
32
+ for (const [name] of [...services, ...this.#services]) {
33
+ this.container.get('logger').debug(`Booting service ${name}`);
34
+ await this.container.get(name).boot();
35
+ }
36
+ }
37
+
38
+ async #registerServices(servicesToRegister = []) {
39
+ for (const [name, serviceClass, args] of servicesToRegister) {
40
+ this.container.register(name, serviceClass, args.map(arg => {
41
+ if(arg === "@service_container") return this.container;
42
+ if(arg === "@onConnection") return this.#onSocketConnection;
43
+ return arg
44
+ }));
45
+ if(this.container.has("logger")) {
46
+ this.container.get("logger").debug(`Service ${name} registered`);
47
+ }
48
+ }
49
+
50
+ this.container.compile();
51
+ }
52
+ }
@@ -0,0 +1,11 @@
1
+ module.exports = class Service {
2
+ name = null;
3
+
4
+ constructor(name) {
5
+ this.name = name;
6
+ }
7
+
8
+ async boot() {
9
+ throw new Error("Method 'boot' must be implemented.");
10
+ }
11
+ }
@@ -0,0 +1,38 @@
1
+ const Service = require("../Service");
2
+ const { createClient } = require("redis");
3
+
4
+ module.exports = class Cache extends Service {
5
+ #container;
6
+ client;
7
+
8
+ constructor(container, url) {
9
+ super("cache");
10
+ this.#container = container;
11
+ this.client = createClient({ url });
12
+ }
13
+
14
+ async boot() {
15
+ await this.client.connect();
16
+ }
17
+
18
+ async set(key, value) {
19
+ return await this.client.set(key, value);
20
+
21
+ }
22
+
23
+ async get(key) {
24
+ return await this.client.get(key);
25
+ }
26
+
27
+ async del(key) {
28
+ return await this.client.del(key);
29
+ }
30
+
31
+ async keys(pattern) {
32
+ return await this.client.keys(pattern);
33
+ }
34
+
35
+ async expire(key, seconds) {
36
+ return await this.client.expire(key, seconds);
37
+ }
38
+ }
@@ -0,0 +1,69 @@
1
+ const { Sequelize } = require("sequelize");
2
+ const Service = require("../Service");
3
+ const fg = require('fast-glob');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ module.exports = class Database extends Service {
8
+ #container;
9
+ #databaseUrl;
10
+ sequelize
11
+ models = {}
12
+
13
+ constructor(container, databaseUrl) {
14
+ super('database');
15
+ this.#container = container;
16
+ this.#databaseUrl = databaseUrl;
17
+ }
18
+
19
+ async boot() {
20
+ this.#createModelsFolder();
21
+ this.sequelize = new Sequelize(process.env.DATABASE_URL, {
22
+ dialect: process.env.DATABASE_DIALECT || 'mariadb',
23
+ logging: (msg) => this.#container.get('logger').debug(msg),
24
+ operatorsAliases: 0,
25
+ pool: {
26
+ max: 40,
27
+ min: 0,
28
+ acquire: 30000,
29
+ idle: 10000
30
+ },
31
+ });
32
+
33
+ await this.sequelize.authenticate();
34
+
35
+ await this.#loadModels();
36
+
37
+ this.#container.get('logger').debug(`Database modesl loaded: ${Object.keys(this.models).join(", ")}`);
38
+ }
39
+
40
+ async #loadModels() {
41
+ const models = await fg('*.js', { cwd: path.join(process.cwd(), "src", "models") });
42
+ for (const model of models) {
43
+ const modelPath = path.join(process.cwd(), "src", "models", model);
44
+ const modelFunc = require(modelPath);
45
+ if (typeof modelFunc !== 'function') {
46
+ this.#container.get('logger').error(`Model ${model} is not a function`);
47
+ continue;
48
+ }
49
+ const modelInstance = modelFunc({sequelize: this.sequelize, container: this.#container, Sequelize});
50
+ this.models[model.replace('.js', '')] = modelInstance;
51
+ }
52
+
53
+ for (const model of Object.values(this.models)) {
54
+ if (model.associate) {
55
+ model.associate(this.models);
56
+ }
57
+ }
58
+ }
59
+
60
+ #createModelsFolder() {
61
+ const path = 'src/models';
62
+ if (!fs.existsSync(path)) fs.mkdirSync(path);
63
+ }
64
+
65
+ sync() {
66
+ return this.sequelize.sync();
67
+ }
68
+
69
+ }
@@ -0,0 +1,12 @@
1
+ const Database = require("./Database");
2
+ const Cache = require("./Cache");
3
+ const S3 = require("./S3");
4
+ const { SocketIO } = require("./socketio");
5
+
6
+ module.exports = [
7
+ ["logger", require("./Logger"), ["@service_container", process.env.LOG_DIRECTORY || `${process.cwd()}/logs`, process.env.DEBUG === "true"]],
8
+ process.env.DATABASE_URL ? ["database", Database, ["@service_container", process.env.DATABASE_URL]] : null,
9
+ process.env.CACHE_URL ? ["cache", Cache, ["@service_container", process.env.CACHE_URL]] : null,
10
+ (process.env.S3_ENDPOINT && process.env.S3_ACCESS_KEY && process.env.S3_SECRET_KEY) ? ["s3", S3, ["@service_container", process.env.S3_ENDPOINT, process.env.S3_PORT, process.env.S3_USE_SSL === "true", process.env.S3_ACCESS_KEY, process.env.S3_SECRET_KEY]] : null,
11
+ ["socketio", SocketIO, ["@service_container", process.env.PORT || 3000]],
12
+ ].filter(Boolean);
@@ -0,0 +1,81 @@
1
+ const fs = require("fs");
2
+ const Service = require("../Service");
3
+ require("colors");
4
+
5
+ module.exports = class Logger extends Service {
6
+ #container
7
+ #logDirectory;
8
+ #debugEnabled;
9
+ messageColors = {
10
+ log: "white",
11
+ info: "green",
12
+ warn: "yellow",
13
+ error: "red",
14
+ debug: "blue"
15
+ };
16
+ #storeTries = 0;
17
+
18
+ constructor(container, logDirectory, debugEnabled) {
19
+ super("logger");
20
+ this.#container = container;
21
+ this.#logDirectory = logDirectory;
22
+ this.#debugEnabled = debugEnabled;
23
+ }
24
+
25
+ async boot() {
26
+ if (!fs.existsSync(this.#logDirectory)) fs.mkdirSync(this.#logDirectory);
27
+ return this;
28
+ }
29
+
30
+ async store(message) {
31
+ if (this.#storeTries > 10) throw new Error("Failed to store log message");
32
+ this.#storeTries++;
33
+ try{
34
+ fs.appendFileSync(`${this.#logDirectory}/${new Date().toISOString().split("T")[0]}.log`, `${message}\n`);
35
+ this.#storeTries = 0;
36
+ } catch (e) {
37
+ if (!fs.existsSync(this.#logDirectory)) {
38
+ fs.mkdirSync(this.#logDirectory);
39
+ }
40
+
41
+ if (!fs.existsSync(`${this.#logDirectory}/${new Date().toISOString().split("T")[0]}.log`)) {
42
+ fs.writeFileSync(`${this.#logDirectory}/${new Date().toISOString().split("T")[0]}.log`, "");
43
+ }
44
+
45
+ return this.store(message);
46
+ }
47
+ }
48
+
49
+ getDateTimestamp() {
50
+ return new Date().toISOString().replace("T", " ").replace("Z", "");
51
+ }
52
+
53
+ buildMessage(type, message) {
54
+ return `${this.getDateTimestamp()} [${type.toUpperCase()}] ${message}`[this.messageColors[type]];
55
+ }
56
+
57
+ async log(message) {
58
+ console.log(this.buildMessage("log", message));
59
+ this.store(this.buildMessage("log", message));
60
+ }
61
+
62
+ async info(message) {
63
+ console.log(this.buildMessage("info", message));
64
+ this.store(this.buildMessage("info", message));
65
+ }
66
+
67
+ async warn(message) {
68
+ console.log(this.buildMessage("warn", message));
69
+ this.store(this.buildMessage("warn", message));
70
+ }
71
+
72
+ async error(message) {
73
+ console.log(this.buildMessage("error", message));
74
+ this.store(this.buildMessage("error", message));
75
+ }
76
+
77
+ async debug(message) {
78
+ this.#debugEnabled && console.log(this.buildMessage("debug", message));
79
+ this.store(this.buildMessage("debug", message));
80
+ }
81
+ }
@@ -0,0 +1,83 @@
1
+ const Service = require("../Service");
2
+ const MinioService = require('minio')
3
+
4
+ module.exports = class S3 extends Service {
5
+ #container
6
+ #endPoint
7
+ #port
8
+ #useSSL
9
+ #accessKey
10
+ #secretKey
11
+
12
+ constructor(container, endPoint, port, useSSL, accessKey, secretKey) {
13
+ super('s3')
14
+ this.#container = container
15
+ this.#endPoint = endPoint
16
+ this.#port = port
17
+ this.#useSSL = useSSL
18
+ this.#accessKey = accessKey
19
+ this.#secretKey = secretKey
20
+ }
21
+
22
+ async boot() {
23
+ this.client = new MinioService.Client({
24
+ endPoint: this.#endPoint,
25
+ port: this.#port,
26
+ useSSL: this.#useSSL,
27
+ accessKey: this.#accessKey,
28
+ secretKey: this.#secretKey
29
+ })
30
+ }
31
+
32
+ async saveFile(bucketName, fileName, file, contentType = 'binary/octet-stream') {
33
+ this.#container.get('logger').debug(`Saving file ${fileName} to bucket ${bucketName}`);
34
+ return new Promise((resolve, reject) => {
35
+ this.client.putObject(
36
+ bucketName,
37
+ fileName,
38
+ file,
39
+ {
40
+ "Content-Type": contentType
41
+ },
42
+ (err, etag) => {
43
+ if (err) return reject(err);
44
+ resolve(etag);
45
+ }
46
+ );
47
+ });
48
+ }
49
+
50
+ async getFile(bucketName, fileName) {
51
+ return new Promise((resolve, reject) => {
52
+ let data = ''
53
+ this.client.getObject(bucketName, fileName, (err, stream) => {
54
+ if(err) return reject(err)
55
+ stream.on('data', (chunk) => {
56
+ data += chunk
57
+ })
58
+ stream.on('end', () => {
59
+ resolve(data)
60
+ })
61
+ })
62
+ })
63
+ }
64
+
65
+ async removeFile(bucketName, fileName) {
66
+ this.#container.get('logger').debug(`Removing file ${fileName} from bucket ${bucketName}`);
67
+ return this.client.removeObject(bucketName, fileName)
68
+ }
69
+
70
+ async createBucket(bucketName) {
71
+ this.#container.get('logger').debug(`Creating bucket ${bucketName}`);
72
+ return this.client.makeBucket(bucketName, 'us-east-1')
73
+ }
74
+
75
+ listBuckets() {
76
+ return new Promise((resolve, reject) => {
77
+ this.client.listBuckets((err, buckets) => {
78
+ if(err) return reject(err)
79
+ resolve(buckets)
80
+ })
81
+ })
82
+ }
83
+ }
@@ -0,0 +1,11 @@
1
+ module.exports = class Handler {
2
+ event;
3
+
4
+ constructor(event) {
5
+ this.event = event;
6
+ }
7
+
8
+ handle({ container, socket, data }) {
9
+ throw new Error("You should implement 'handle()' method on your handler");
10
+ }
11
+ }
@@ -0,0 +1,11 @@
1
+ module.exports = class Middleware {
2
+ name;
3
+
4
+ constructor(name) {
5
+ this.name = name;
6
+ }
7
+
8
+ async handle({ container, socket}) {
9
+ throw new Error("Method 'handle' must be implemented.");
10
+ }
11
+ }
@@ -0,0 +1,138 @@
1
+ const Service = require("../Service");
2
+ const { Server } = require("socket.io");
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const fg = require('fast-glob');
6
+ const Handler = require("./Handler");
7
+ const Middleware = require("./Middleware");
8
+
9
+
10
+ module.exports = class SocketIO extends Service {
11
+ port;
12
+ #container;
13
+ io;
14
+ middlewares = {};
15
+
16
+ constructor(container, port, onConnection = () => { }) {
17
+ super("socketio");
18
+ this.port = port || 3000;
19
+ this.#container = container;
20
+ }
21
+
22
+ async boot() {
23
+ const middlewares = await this.#loadMiddlewaresFromFolder(path.join(process.cwd(), "src", "middlewares"));
24
+ for (const middleware of middlewares) {
25
+ this.middlewares[middleware.name] = middleware;
26
+ }
27
+ const handlers = await this.#loadHandlersFromFolder(path.join(process.cwd(), "src", "handlers"));
28
+ const connectionHandler = new (await this.#loadConnectionHandler())();
29
+
30
+
31
+ this.#container.get('logger').debug(`Middlewares: ${middlewares.map(mdl => mdl.name).join(", ")}`);
32
+ this.#container.get('logger').debug(`Handlers: ${handlers.map(hdl => hdl.event).join(", ")}`);
33
+
34
+ this.io = new Server({ cors: { origin: "*" }, maxHttpBufferSize: 10 * 1024 * 1024 });
35
+
36
+ this.io.on("connection", (socket) => {
37
+ const connectionMiddlewares = (connectionHandler?.middlewares || []).map(mdl => this.middlewares[mdl])
38
+ for (const middleware of connectionMiddlewares) {
39
+ if(!middleware) {
40
+ this.#container.get('logger').warn(`You are using a middleware that does not exist`);
41
+ continue;
42
+ }
43
+ middleware.handle({ container: this.#container, socket });
44
+ }
45
+ connectionHandler.handle({ container: this.#container, socket });
46
+ handlers.forEach((handler) => {
47
+ const handlerMiddlewares = (handler?.middlewares || []).map(mdl => this.middlewares[mdl])
48
+ socket.on(handler.event, async (data) => {
49
+ this.#container.get('logger').debug(`[${socket?.id}] Sent ${handler.event} with middlewares: ${handlerMiddlewares?.map(mdl => mdl?.name).join(", ")}`);
50
+ for (const middleware of handlerMiddlewares) {
51
+ if(!middleware) {
52
+ this.#container.get('logger').warn(`You are using a middleware that does not exist`);
53
+ continue;
54
+ }
55
+ await middleware.handle({ container: this.#container, socket });
56
+ }
57
+ return await handler.handle({ container: this.#container, socket, data });
58
+ });
59
+ });
60
+ });
61
+
62
+ this.io.listen(this.port);
63
+ this.#container.get('logger').info(`Socket.IO is running on port ws://localhost:${this.port}`);
64
+ }
65
+
66
+ async #loadConnectionHandler() {
67
+ const connectionHandlerExists = fs.existsSync(path.join(process.cwd(), "src", "handlers", "connection.js"));
68
+ /* if exists require, otherwise create a default one */
69
+ if(!connectionHandlerExists) {
70
+ fs.writeFileSync(path.join(process.cwd(), "src", "handlers", "connection.js"), `const { Handler } = require("zyket");
71
+
72
+ module.exports = class ConnectionHandler extends Handler {
73
+ async handle({ container, socket }) {
74
+ container.get("logger").info("New connection");
75
+ }
76
+ };`);
77
+ }
78
+ return require(path.join(process.cwd(), "src", "handlers", "connection.js"));
79
+ }
80
+
81
+ async #loadHandlersFromFolder(handlersFolder) {
82
+ this.#createHandlersFolder(handlersFolder);
83
+ // need to read all files and subfolders
84
+ const handlers = (await fg('**/*.js', { cwd: handlersFolder, ignore: 'connection.js' })).map((hdr) => {
85
+ // should be type handler
86
+ const handler = require(path.join(handlersFolder, hdr));
87
+ if(!(handler.prototype instanceof Handler)) throw new Error(`${hdr} is not a valid handler`);
88
+ return new handler(hdr.replace('.js', ''));
89
+ });
90
+ return handlers;
91
+ }
92
+
93
+ #createHandlersFolder(handlersFolder, overwrite = false) {
94
+ if (fs.existsSync(handlersFolder) && !overwrite) return;
95
+ this.#container.get('logger').info(`Creating handlers folder at ${handlersFolder}`);
96
+ fs.mkdirSync(handlersFolder);
97
+ // create a default handler
98
+ fs.writeFileSync(path.join(handlersFolder, "message.js"), `const { Handler } = require("zyket");
99
+ module.exports = class MessageHandler extends Handler {
100
+ event = "message";
101
+ middlewares = ["default"];
102
+
103
+ async handle({ container, socket, data }) {
104
+ container.get("logger").info("Message handler");
105
+ }
106
+ };`);
107
+ }
108
+
109
+ async #loadMiddlewaresFromFolder(middlewaresFolder) {
110
+ this.#createMiddlewaresFolder(middlewaresFolder);
111
+ // need to read all files and subfolders
112
+ const middlewares = await fg('**/*.js', { cwd: middlewaresFolder })
113
+
114
+ return middlewares.map((mdl) => {
115
+ // should be type middleware
116
+ const middleware = require(path.join(middlewaresFolder, mdl));
117
+ if(!(middleware.prototype instanceof Middleware)) throw new Error(`${mdl} is not a valid middleware`);
118
+ return new middleware(mdl.replace('.js', ''));
119
+ });
120
+ }
121
+
122
+ #createMiddlewaresFolder(middlewaresFolder, overwrite = false) {
123
+ if (fs.existsSync(middlewaresFolder) && !overwrite) return;
124
+ this.#container.get('logger').info(`Creating middlewares folder at ${middlewaresFolder}`);
125
+ fs.mkdirSync(middlewaresFolder);
126
+ // create a default middleware
127
+ fs.writeFileSync(path.join(middlewaresFolder, "default.js"), `const { Middleware } = require("zyket");
128
+ module.exports = class DefaultMiddleware extends Middleware {
129
+ async handle({ container, socket }) {
130
+ container.get("logger").info("Default middleware");
131
+ }
132
+ };`);
133
+ }
134
+
135
+ stop() {
136
+ this.io.close();
137
+ }
138
+ }
@@ -0,0 +1,5 @@
1
+ module.exports = {
2
+ SocketIO: require("./SocketIO"),
3
+ Handler: require("./Handler"),
4
+ Middleware: require("./Middleware")
5
+ }
@@ -0,0 +1,32 @@
1
+ const fs = require('fs');
2
+
3
+ module.exports = class EnvManager {
4
+ static load(secretsPath) {
5
+ this.createEnvFile(secretsPath);
6
+ require("dotenv").config({ path: secretsPath });
7
+ }
8
+
9
+ static createEnvFile(secretsPath, overwrite = false) {
10
+ if (fs.existsSync(secretsPath) && !overwrite) return;
11
+ fs.writeFileSync(secretsPath, this.getDefaultSecrets());
12
+ }
13
+
14
+ static getDefaultSecrets() {
15
+ const envsToCreate = {
16
+ DEBUG: true,
17
+ PORT: 3000,
18
+ DATABASE_URL: '',
19
+ CACHE_URL: '',
20
+ S3_ENDPOINT: '',
21
+ S3_PORT: '',
22
+ S3_USE_SSL: true,
23
+ S3_ACCESS_KEY: '',
24
+ S3_SECRET_KEY: '',
25
+ LOG_DIRECTORY: "./logs"
26
+ }
27
+
28
+ return Object.entries(envsToCreate).reduce((acc, [key, value]) => {
29
+ return `${acc}${key}=${value}\n`;
30
+ }, "");
31
+ }
32
+ }