@seip/blue-bird 0.1.0
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/.env_example +12 -0
- package/.gitattributes +2 -0
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/backend/index.js +13 -0
- package/backend/routes/app.js +41 -0
- package/core/app.js +150 -0
- package/core/auth.js +59 -0
- package/core/cli/init.js +116 -0
- package/core/cli/react.js +394 -0
- package/core/config.js +42 -0
- package/core/logger.js +80 -0
- package/core/middleware.js +27 -0
- package/core/router.js +96 -0
- package/core/template.js +223 -0
- package/core/upload.js +70 -0
- package/core/validate.js +275 -0
- package/package.json +39 -0
package/core/template.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import ejs from "ejs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import Config from "./config.js";
|
|
5
|
+
import Logger from "./logger.js";
|
|
6
|
+
|
|
7
|
+
const __dirname = Config.dirname();
|
|
8
|
+
const props = Config.props();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Template engine wrapper using EJS, providing helper functions and React island support.
|
|
12
|
+
*/
|
|
13
|
+
class Template {
|
|
14
|
+
/**
|
|
15
|
+
* Renders an EJS template with the provided context and helper functions.
|
|
16
|
+
* @param {string} template - The template name (without .ejs extension).
|
|
17
|
+
* @param {Object} [context={}] - Data to pass to the template.
|
|
18
|
+
* @param {import('express').Response} res - The Express response object.
|
|
19
|
+
* @returns {Promise<void>}
|
|
20
|
+
*/
|
|
21
|
+
static async render(template, context = {}, res) {
|
|
22
|
+
try {
|
|
23
|
+
const templatePath = path.join(__dirname, "templates", `${template}.ejs`);
|
|
24
|
+
|
|
25
|
+
const helpers = {
|
|
26
|
+
asset: (file) => this.asset(file),
|
|
27
|
+
url: (p = "") => this.url(p),
|
|
28
|
+
react: (component, props = {}) => this.react(component, props),
|
|
29
|
+
vite_assets: () => this.vite_assets()
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const fullContext = {
|
|
33
|
+
...props,
|
|
34
|
+
...helpers,
|
|
35
|
+
...context
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const html = await ejs.renderFile(templatePath, fullContext);
|
|
39
|
+
const minifiedHtml = this.minifyHtml(html);
|
|
40
|
+
|
|
41
|
+
res.send(minifiedHtml);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const logger = new Logger();
|
|
44
|
+
logger.error(`Template render error: ${error.message}`);
|
|
45
|
+
|
|
46
|
+
if (props.debug) {
|
|
47
|
+
res.status(500).send(`<pre>${error.stack}</pre>`);
|
|
48
|
+
} else {
|
|
49
|
+
res.status(500).send("Internal Server Error");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Generates a URL for a static asset.
|
|
56
|
+
* @param {string} file - The asset path.
|
|
57
|
+
* @returns {string} The full asset URL.
|
|
58
|
+
*/
|
|
59
|
+
static asset(file) {
|
|
60
|
+
return `${props.host}:${props.port}/public/${file.replace(/^\//, "")}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Generates a full URL for the given path.
|
|
65
|
+
* @param {string} [p=""] - The relative path.
|
|
66
|
+
* @returns {string} The full URL.
|
|
67
|
+
*/
|
|
68
|
+
static url(p = "") {
|
|
69
|
+
const cleanPath = p.replace(/^\//, "");
|
|
70
|
+
return `${props.host}:${props.port}/${cleanPath}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Renders a React component as an HTML string.
|
|
75
|
+
* @param {string} component - The React component name.
|
|
76
|
+
* @param {Object} [componentProps={}] - Props to pass to the component.
|
|
77
|
+
* @param {string} [classBody=""] - Class name to add to the body tag.
|
|
78
|
+
* @param {Array<Object>} [optionsHead=[]] - Array of objects with tag and attributes for head tags.
|
|
79
|
+
* @returns {string} The HTML string of the React component.
|
|
80
|
+
*/
|
|
81
|
+
static renderReact(res, component = "App", propsReact = {}, classBody = "", optionsHead = []) {
|
|
82
|
+
const html = `
|
|
83
|
+
<!DOCTYPE html>
|
|
84
|
+
<html lang="${props.langMeta}">
|
|
85
|
+
<head>
|
|
86
|
+
<meta charset="UTF-8">
|
|
87
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
88
|
+
<title>${props.titleMeta}</title>
|
|
89
|
+
<meta name="description" content="${props.descriptionMeta}">
|
|
90
|
+
<meta name="keywords" content="${props.keywordsMeta}">
|
|
91
|
+
<meta name="author" content="${props.authorMeta}">
|
|
92
|
+
${optionsHead.map(item => `<${item.tag} ${Object.entries(item.attrs).map(([key, value]) => `${key}="${value}"`).join(" ")}></${item.tag}>`).join("")}
|
|
93
|
+
</head>
|
|
94
|
+
<body class="${classBody}">
|
|
95
|
+
${this.react(component, propsReact)}
|
|
96
|
+
${this.vite_assets()}
|
|
97
|
+
</body>
|
|
98
|
+
</html>
|
|
99
|
+
`
|
|
100
|
+
res.type("text/html");
|
|
101
|
+
res.status(200);
|
|
102
|
+
res.setHeader("Content-Type", "text/html");
|
|
103
|
+
return res.send(this.minifyHtml(html));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Generates a container for a React island component.
|
|
108
|
+
* @param {string} component - The React component name.
|
|
109
|
+
* @param {Object} [componentProps={}] - Props to pass to the component.
|
|
110
|
+
* @returns {string} The HTML container with data attributes for hydration.
|
|
111
|
+
*/
|
|
112
|
+
static react(component, componentProps = {}) {
|
|
113
|
+
const id = `react-${Math.random().toString(36).substr(2, 9)}`;
|
|
114
|
+
const propsJson = JSON.stringify(componentProps).replace(/'/g, "'");
|
|
115
|
+
return `<div id="${id}" data-react-component="${component}" data-props='${propsJson}'></div>`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Generates script and link tags for Vite-managed assets.
|
|
120
|
+
* Handles both development (Vite dev server) and production (manifest.json) modes.
|
|
121
|
+
* @returns {string} The HTML tags for scripts and styles.
|
|
122
|
+
*/
|
|
123
|
+
static vite_assets() {
|
|
124
|
+
|
|
125
|
+
if (props.debug) {
|
|
126
|
+
return `
|
|
127
|
+
<script type="module">
|
|
128
|
+
import RefreshRuntime from "http://localhost:5173/build/@react-refresh";
|
|
129
|
+
RefreshRuntime.injectIntoGlobalHook(window);
|
|
130
|
+
window.$RefreshReg$ = () => {};
|
|
131
|
+
window.$RefreshSig$ = () => (type) => type;
|
|
132
|
+
window.__vite_plugin_react_preamble_installed__ = true;
|
|
133
|
+
</script>
|
|
134
|
+
<script type="module" src="http://localhost:5173/build/@vite/client"></script>
|
|
135
|
+
<script type="module" src="http://localhost:5173/build/Main.jsx"></script>`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const buildPath = path.join(__dirname, props.static.path, "build");
|
|
139
|
+
let manifestPath = path.join(buildPath, "manifest.json");
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
if (!fs.existsSync(manifestPath)) {
|
|
143
|
+
manifestPath = path.join(buildPath, ".vite", "manifest.json");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (fs.existsSync(manifestPath)) {
|
|
147
|
+
try {
|
|
148
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
149
|
+
const entry = manifest["Main.jsx"];
|
|
150
|
+
if (entry) {
|
|
151
|
+
const file = entry.file;
|
|
152
|
+
const css = entry.css || [];
|
|
153
|
+
|
|
154
|
+
let html = `<script type="module" src="/build/${file}"></script>`;
|
|
155
|
+
css.forEach(cssFile => {
|
|
156
|
+
html += `<link rel="stylesheet" href="/build/${cssFile}">`;
|
|
157
|
+
});
|
|
158
|
+
return html;
|
|
159
|
+
}
|
|
160
|
+
} catch (e) {
|
|
161
|
+
return `<!-- Error parsing Vite manifest: ${e.message} -->`;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return `<!-- Vite Manifest not found at ${manifestPath} -->`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Renders a full HTML page with a React component as the main entry point.
|
|
170
|
+
* Useful for SPAs or full-page React modules.
|
|
171
|
+
* @param {import('express').Response} res - The Express response object.
|
|
172
|
+
* @param {string} component - The React component name.
|
|
173
|
+
* @param {Object} [componentProps={}] - Props to pass to the React component.
|
|
174
|
+
* @param {Object} [metaOverrides={}] - Metadata overrides (title, description, keywords, etc.).
|
|
175
|
+
*/
|
|
176
|
+
static reactRender(res, component, componentProps = {}, metaOverrides = {}) {
|
|
177
|
+
const meta = {
|
|
178
|
+
title: props.titleMeta,
|
|
179
|
+
description: props.descriptionMeta,
|
|
180
|
+
keywords: props.keywordsMeta,
|
|
181
|
+
author: props.authorMeta,
|
|
182
|
+
lang: props.langMeta,
|
|
183
|
+
...metaOverrides
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const scriptsReact = this.vite_assets();
|
|
187
|
+
const componentHtml = this.react(component, componentProps);
|
|
188
|
+
|
|
189
|
+
const html = `
|
|
190
|
+
<!DOCTYPE html>
|
|
191
|
+
<html lang="${meta.lang}">
|
|
192
|
+
<head>
|
|
193
|
+
<meta charset="UTF-8">
|
|
194
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
195
|
+
<title>${meta.title}</title>
|
|
196
|
+
<meta name="description" content="${meta.description}">
|
|
197
|
+
<meta name="keywords" content="${meta.keywords}">
|
|
198
|
+
<meta name="author" content="${meta.author}">
|
|
199
|
+
${scriptsReact}
|
|
200
|
+
</head>
|
|
201
|
+
<body>
|
|
202
|
+
${componentHtml}
|
|
203
|
+
</body>
|
|
204
|
+
</html>`;
|
|
205
|
+
|
|
206
|
+
res.send(this.minifyHtml(html));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Minifies the HTML output by removing comments and excessive whitespace.
|
|
211
|
+
* @param {string} html - The raw HTML string.
|
|
212
|
+
* @returns {string} The minified HTML string.
|
|
213
|
+
*/
|
|
214
|
+
static minifyHtml(html) {
|
|
215
|
+
return html
|
|
216
|
+
.replace(/<!--(?!\[if).*?-->/gs, "")
|
|
217
|
+
.replace(/>\s+</g, "><")
|
|
218
|
+
.replace(/\s{2,}/g, " ")
|
|
219
|
+
.trim();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export default Template;
|
package/core/upload.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import multer from "multer";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import Config from "./config.js";
|
|
5
|
+
|
|
6
|
+
const __dirname = Config.dirname();
|
|
7
|
+
const props = Config.props();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Upload helper to manage file uploads using multer.
|
|
11
|
+
*/
|
|
12
|
+
class Upload {
|
|
13
|
+
/**
|
|
14
|
+
* Configures storage for uploaded files.
|
|
15
|
+
* @param {string} folder - The destination folder within the static path.
|
|
16
|
+
* @returns {import('multer').StorageEngine}
|
|
17
|
+
*/
|
|
18
|
+
static storage(folder = "uploads") {
|
|
19
|
+
const dest = path.join(__dirname, props.static.path, folder);
|
|
20
|
+
|
|
21
|
+
if (!fs.existsSync(dest)) {
|
|
22
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return multer.diskStorage({
|
|
26
|
+
destination: (req, file, cb) => {
|
|
27
|
+
cb(null, dest);
|
|
28
|
+
},
|
|
29
|
+
filename: (req, file, cb) => {
|
|
30
|
+
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
|
|
31
|
+
cb(null, uniqueSuffix + path.extname(file.originalname));
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Returns a multer instance for single or multiple file uploads.
|
|
38
|
+
* @param {Object} options - Multer options.
|
|
39
|
+
* @param {string} [options.folder='uploads'] - Destination folder.
|
|
40
|
+
* @param {number} [options.fileSize=5000000] - Max file size in bytes (default 5MB).
|
|
41
|
+
* @param {Array<string>} [options.allowedTypes=[]] - Allowed mime types (e.g. ['image/png', 'image/jpeg']).
|
|
42
|
+
* @returns {import('multer').Multer}
|
|
43
|
+
*/
|
|
44
|
+
static disk(options = {}) {
|
|
45
|
+
const { folder = "uploads", fileSize = 5000000, allowedTypes = [] } = options;
|
|
46
|
+
|
|
47
|
+
return multer({
|
|
48
|
+
storage: this.storage(folder),
|
|
49
|
+
limits: { fileSize },
|
|
50
|
+
fileFilter: (req, file, cb) => {
|
|
51
|
+
if (allowedTypes.length > 0 && !allowedTypes.includes(file.mimetype)) {
|
|
52
|
+
return cb(new Error("File type not allowed"), false);
|
|
53
|
+
}
|
|
54
|
+
cb(null, true);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Helper to get the public URL of an uploaded file.
|
|
61
|
+
* @param {string} filename - The name of the file.
|
|
62
|
+
* @param {string} [folder='uploads'] - The folder where the file is stored.
|
|
63
|
+
* @returns {string} The full public URL.
|
|
64
|
+
*/
|
|
65
|
+
static url(filename, folder = "uploads") {
|
|
66
|
+
return `${props.host}:${props.port}/public/${folder}/${filename}`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default Upload;
|
package/core/validate.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
const messages_default = {
|
|
2
|
+
es: {
|
|
3
|
+
required: (f) => `El campo ${f} es obligatorio`,
|
|
4
|
+
min: (f, n) => `El campo ${f} debe tener al menos ${n} caracteres`,
|
|
5
|
+
max: (f, n) => `El campo ${f} no puede tener más de ${n} caracteres`,
|
|
6
|
+
email: (f) => `El campo ${f} debe ser un email válido`,
|
|
7
|
+
number: (f) => `El campo ${f} debe ser numérico`,
|
|
8
|
+
alpha: (f) => `El campo ${f} solo puede contener letras`,
|
|
9
|
+
alphanumeric: (f) => `El campo ${f} solo puede contener letras y números`,
|
|
10
|
+
boolean: (f) => `El campo ${f} debe ser verdadero o falso`,
|
|
11
|
+
date: (f) => `El campo ${f} debe ser una fecha válida`,
|
|
12
|
+
url: (f) => `El campo ${f} debe ser una URL válida`,
|
|
13
|
+
in: (f, v) => `El campo ${f} debe ser uno de: ${v.join(', ')}`,
|
|
14
|
+
equals: (f, v) => `El campo ${f} debe ser igual a ${v}`,
|
|
15
|
+
password: () => `La contraseña debe tener mayúsculas, minúsculas y números`,
|
|
16
|
+
pattern: (f) => `El campo ${f} no cumple el patrón requerido`
|
|
17
|
+
},
|
|
18
|
+
en: {
|
|
19
|
+
required: (f) => `${f} is required`,
|
|
20
|
+
min: (f, n) => `${f} must be at least ${n} characters`,
|
|
21
|
+
max: (f, n) => `${f} must be at most ${n} characters`,
|
|
22
|
+
email: (f) => `${f} must be a valid email`,
|
|
23
|
+
number: (f) => `${f} must be numeric`,
|
|
24
|
+
alpha: (f) => `${f} must contain only letters`,
|
|
25
|
+
alphanumeric: (f) => `${f} must contain only letters and numbers`,
|
|
26
|
+
boolean: (f) => `${f} must be true or false`,
|
|
27
|
+
date: (f) => `${f} must be a valid date`,
|
|
28
|
+
url: (f) => `${f} must be a valid URL`,
|
|
29
|
+
in: (f, v) => `${f} must be one of: ${v.join(', ')}`,
|
|
30
|
+
equals: (f, v) => `${f} must equal ${v}`,
|
|
31
|
+
password: () => `Password must contain uppercase, lowercase and numbers`,
|
|
32
|
+
pattern: (f) => `${f} does not match the required pattern`
|
|
33
|
+
},
|
|
34
|
+
pt: {
|
|
35
|
+
required: (f) => `O campo ${f} é obrigatório`,
|
|
36
|
+
min: (f, n) => `O campo ${f} deve ter pelo menos ${n} caracteres`,
|
|
37
|
+
max: (f, n) => `O campo ${f} não pode ter mais de ${n} caracteres`,
|
|
38
|
+
email: (f) => `O campo ${f} deve ser um email válido`,
|
|
39
|
+
number: (f) => `O campo ${f} deve ser numérico`,
|
|
40
|
+
alpha: (f) => `O campo ${f} só pode conter letras`,
|
|
41
|
+
alphanumeric: (f) => `O campo ${f} só pode conter letras e números`,
|
|
42
|
+
boolean: (f) => `O campo ${f} deve ser verdadeiro ou falso`,
|
|
43
|
+
date: (f) => `O campo ${f} deve ser uma data válida`,
|
|
44
|
+
url: (f) => `O campo ${f} deve ser uma URL válida`,
|
|
45
|
+
in: (f, v) => `O campo ${f} deve ser um de: ${v.join(', ')}`,
|
|
46
|
+
equals: (f, v) => `O campo ${f} deve ser igual a ${v}`,
|
|
47
|
+
password: () => `A senha deve conter maiúsculas, minúsculas e números`,
|
|
48
|
+
pattern: (f) => `O campo ${f} não corresponde ao padrão exigido`
|
|
49
|
+
},
|
|
50
|
+
br: {
|
|
51
|
+
required: (f) => `O campo ${f} é obrigatório`,
|
|
52
|
+
min: (f, n) => `O campo ${f} deve ter pelo menos ${n} caracteres`,
|
|
53
|
+
max: (f, n) => `O campo ${f} não pode ter mais de ${n} caracteres`,
|
|
54
|
+
email: (f) => `O campo ${f} deve ser um email válido`,
|
|
55
|
+
number: (f) => `O campo ${f} deve ser numérico`,
|
|
56
|
+
alpha: (f) => `O campo ${f} só pode conter letras`,
|
|
57
|
+
alphanumeric: (f) => `O campo ${f} só pode conter letras e números`,
|
|
58
|
+
boolean: (f) => `O campo ${f} deve ser verdadeiro ou falso`,
|
|
59
|
+
date: (f) => `O campo ${f} deve ser uma data válida`,
|
|
60
|
+
url: (f) => `O campo ${f} deve ser uma URL válida`,
|
|
61
|
+
in: (f, v) => `O campo ${f} deve ser um de: ${v.join(', ')}`,
|
|
62
|
+
equals: (f, v) => `O campo ${f} deve ser igual a ${v}`,
|
|
63
|
+
password: () => `A senha deve conter maiúsculas, minúsculas e números`,
|
|
64
|
+
pattern: (f) => `O campo ${f} não corresponde ao padrão exigido`
|
|
65
|
+
},
|
|
66
|
+
fr: {
|
|
67
|
+
required: (f) => `Le champ ${f} est obligatoire`,
|
|
68
|
+
min: (f, n) => `Le champ ${f} doit contenir au moins ${n} caractères`,
|
|
69
|
+
max: (f, n) => `Le champ ${f} ne peut pas contenir plus de ${n} caractères`,
|
|
70
|
+
email: (f) => `Le champ ${f} doit être un email valide`,
|
|
71
|
+
number: (f) => `Le champ ${f} doit être numérique`,
|
|
72
|
+
alpha: (f) => `Le champ ${f} ne peut contenir que des lettres`,
|
|
73
|
+
alphanumeric: (f) => `Le champ ${f} ne peut contenir que des lettres et des chiffres`,
|
|
74
|
+
boolean: (f) => `Le champ ${f} doit être vrai ou faux`,
|
|
75
|
+
date: (f) => `Le champ ${f} doit être une date valide`,
|
|
76
|
+
url: (f) => `Le champ ${f} doit être une URL valide`,
|
|
77
|
+
in: (f, v) => `Le champ ${f} doit être l'un de: ${v.join(', ')}`,
|
|
78
|
+
equals: (f, v) => `Le champ ${f} doit être égal à ${v}`,
|
|
79
|
+
password: () => `Le mot de passe doit contenir des majuscules, des minuscules et des chiffres`,
|
|
80
|
+
pattern: (f) => `Le champ ${f} ne correspond pas au modèle requis`
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const validators = {
|
|
85
|
+
isEmpty: (value) => value === undefined || value === null || value === '',
|
|
86
|
+
isEmail: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
|
|
87
|
+
isNumeric: (value) => !isNaN(value) && !isNaN(parseFloat(value)),
|
|
88
|
+
isAlpha: (value) => /^[a-zA-Z]+$/.test(value),
|
|
89
|
+
isAlphanumeric: (value) => /^[a-zA-Z0-9]+$/.test(value),
|
|
90
|
+
isBoolean: (value) => value === true || value === false || value === 'true' || value === 'false',
|
|
91
|
+
isISO8601: (value) => !isNaN(Date.parse(value)),
|
|
92
|
+
isURL: (value) => {
|
|
93
|
+
try {
|
|
94
|
+
new URL(value);
|
|
95
|
+
return true;
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
isLength: (value, { min, max }) => {
|
|
101
|
+
const len = String(value).length;
|
|
102
|
+
if (min !== undefined && len < min) return false;
|
|
103
|
+
if (max !== undefined && len > max) return false;
|
|
104
|
+
return true;
|
|
105
|
+
},
|
|
106
|
+
isIn: (value, values) => values.includes(value),
|
|
107
|
+
equals: (value, comparison) => value === comparison,
|
|
108
|
+
matches: (value, pattern) => pattern.test(value)
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Comprehensive Validator class for handling multi-language data validation.
|
|
113
|
+
*/
|
|
114
|
+
class Validator {
|
|
115
|
+
/**
|
|
116
|
+
* Initializes the Validator instance with a schema and optional language settings.
|
|
117
|
+
* @param {Object} schema - Validation rules for each field (e.g., { email: { required: true, email: true } }).
|
|
118
|
+
* @param {string} [lang_default=null] - Default language for error messages (e.g., "en", "es").
|
|
119
|
+
* @param {Object} [messages=null] - Custom message overrides for validation rules.
|
|
120
|
+
*/
|
|
121
|
+
constructor(schema, lang_default = null, messages = null) {
|
|
122
|
+
this.schema = schema;
|
|
123
|
+
this.lang_default = lang_default;
|
|
124
|
+
this.messages = messages ? messages : messages_default;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Validates the request body against the defined schema.
|
|
129
|
+
* @param {import('express').Request} req - The Express request object containing the body to validate.
|
|
130
|
+
* @returns {Promise<{success: boolean, error: boolean, errors: Array<{field: string, message: string}>, message: Array<string>, html: Array<string>}>} Validation results.
|
|
131
|
+
*/
|
|
132
|
+
async validate(req) {
|
|
133
|
+
const lang = this.lang_default ? this.lang_default : req?.session?.lang || "es";
|
|
134
|
+
const msg = this.messages[lang] || this.messages.es;
|
|
135
|
+
const errors = [];
|
|
136
|
+
const messages = [];
|
|
137
|
+
const body = req.body || {};
|
|
138
|
+
|
|
139
|
+
for (const [field, config] of Object.entries(this.schema)) {
|
|
140
|
+
const value = body[field];
|
|
141
|
+
|
|
142
|
+
if (config.required && validators.isEmpty(value)) {
|
|
143
|
+
messages.push(config.messages?.required || msg.required(field));
|
|
144
|
+
errors.push({
|
|
145
|
+
field: field,
|
|
146
|
+
message: config.messages?.required || msg.required(field)
|
|
147
|
+
})
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!validators.isEmpty(value)) {
|
|
152
|
+
if (config.min && !validators.isLength(value, { min: config.min })) {
|
|
153
|
+
messages.push(config.messages?.min || msg.min(field, config.min));
|
|
154
|
+
errors.push({
|
|
155
|
+
field: field,
|
|
156
|
+
message: config.messages?.min || msg.min(field, config.min)
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
if (config.max && !validators.isLength(value, { max: config.max })) {
|
|
160
|
+
messages.push(config.messages?.max || msg.max(field, config.max));
|
|
161
|
+
errors.push({
|
|
162
|
+
field: field,
|
|
163
|
+
message: config.messages?.max || msg.max(field, config.max)
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
if (config.email && !validators.isEmail(value)) {
|
|
167
|
+
messages.push(config.messages?.email || msg.email(field));
|
|
168
|
+
errors.push({
|
|
169
|
+
field: field,
|
|
170
|
+
message: config.messages?.email || msg.email(field)
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
if (config.number && !validators.isNumeric(value)) {
|
|
174
|
+
messages.push(config.messages?.number || msg.number(field));
|
|
175
|
+
errors.push({
|
|
176
|
+
field: field,
|
|
177
|
+
message: config.messages?.number || msg.number(field)
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
if (config.alpha && !validators.isAlpha(value)) {
|
|
181
|
+
messages.push(config.messages?.alpha || msg.alpha(field));
|
|
182
|
+
errors.push({
|
|
183
|
+
field: field,
|
|
184
|
+
message: config.messages?.alpha || msg.alpha(field)
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
if (config.alphanumeric && !validators.isAlphanumeric(value)) {
|
|
188
|
+
messages.push(config.messages?.alphanumeric || msg.alphanumeric(field));
|
|
189
|
+
errors.push({
|
|
190
|
+
field: field,
|
|
191
|
+
message: config.messages?.alphanumeric || msg.alphanumeric(field)
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
if (config.boolean && !validators.isBoolean(value)) {
|
|
195
|
+
messages.push(config.messages?.boolean || msg.boolean(field));
|
|
196
|
+
errors.push({
|
|
197
|
+
field: field,
|
|
198
|
+
message: config.messages?.boolean || msg.boolean(field)
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
if (config.date && !validators.isISO8601(value)) {
|
|
202
|
+
messages.push(config.messages?.date || msg.date(field));
|
|
203
|
+
errors.push({
|
|
204
|
+
field: field,
|
|
205
|
+
message: config.messages?.date || msg.date(field)
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
if (config.url && !validators.isURL(value)) {
|
|
209
|
+
messages.push(config.messages?.url || msg.url(field));
|
|
210
|
+
errors.push({
|
|
211
|
+
field: field,
|
|
212
|
+
message: config.messages?.url || msg.url(field)
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
if (config.in && !validators.isIn(value, config.in)) {
|
|
216
|
+
messages.push(config.messages?.in || msg.in(field, config.in));
|
|
217
|
+
errors.push({
|
|
218
|
+
field: field,
|
|
219
|
+
message: config.messages?.in || msg.in(field, config.in)
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
if (config.equals !== undefined && !validators.equals(value, config.equals)) {
|
|
223
|
+
messages.push(config.messages?.equals || msg.equals(field, config.equals));
|
|
224
|
+
errors.push({
|
|
225
|
+
field: field,
|
|
226
|
+
message: config.messages?.equals || msg.equals(field, config.equals)
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
if (config.password && !validators.matches(value, /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d).{6,}$/)) {
|
|
230
|
+
messages.push(config.messages?.password || msg.password(field));
|
|
231
|
+
errors.push({
|
|
232
|
+
field: field,
|
|
233
|
+
message: config.messages?.password || msg.password(field)
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
if (config.pattern && !validators.matches(value, config.pattern)) {
|
|
237
|
+
messages.push(config.messages?.pattern || msg.pattern(field));
|
|
238
|
+
errors.push({
|
|
239
|
+
field: field,
|
|
240
|
+
message: config.messages?.pattern || msg.pattern(field)
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (errors.length > 0 || messages.length > 0) {
|
|
247
|
+
return {
|
|
248
|
+
success: false,
|
|
249
|
+
error: true,
|
|
250
|
+
errors: errors,
|
|
251
|
+
message: messages,
|
|
252
|
+
html: messages.map(e => `<p class="text-red-500 text-danger">${e}</p>`)
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return { success: true, error: false, errors: [], message: [], html: [] };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Express middleware for automated validation of the request body.
|
|
261
|
+
* Returns a 400 Bad Request response with validation results if errors occur.
|
|
262
|
+
* @returns {Function} Express middleware function (req, res, next).
|
|
263
|
+
*/
|
|
264
|
+
middleware() {
|
|
265
|
+
return async (req, res, next) => {
|
|
266
|
+
const result = await this.validate(req);
|
|
267
|
+
if (!result.success) {
|
|
268
|
+
return res.status(400).json(result);
|
|
269
|
+
}
|
|
270
|
+
next();
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export default Validator
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@seip/blue-bird",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Framework js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"blue-bird": "core/cli/init.js"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/seip25/Blue-bird.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"framework",
|
|
15
|
+
"js"
|
|
16
|
+
],
|
|
17
|
+
"author": "Seip25",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/seip25/Blue-bird/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://seip25.github.io/Blue-bird/",
|
|
23
|
+
"scripts": {
|
|
24
|
+
"dev": "node --watch --env-file=.env backend/index.js",
|
|
25
|
+
"start": "node --env-file=.env backend/index.js",
|
|
26
|
+
"create-react-app": "node core/cli/react.js",
|
|
27
|
+
"react": "node core/cli/react.js",
|
|
28
|
+
"init": "node core/cli/init.js"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"chalk": "^5.6.2",
|
|
32
|
+
"cookie-parser": "^1.4.7",
|
|
33
|
+
"cors": "^2.8.6",
|
|
34
|
+
"ejs": "^4.0.1",
|
|
35
|
+
"express": "^5.2.1",
|
|
36
|
+
"jsonwebtoken": "^9.0.2",
|
|
37
|
+
"multer": "^2.0.2"
|
|
38
|
+
}
|
|
39
|
+
}
|