@syntay/fastay 0.1.7 → 0.1.9

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/dist/app.d.ts CHANGED
@@ -75,6 +75,45 @@ export type CreateAppOptions = {
75
75
  * Default: "/api"
76
76
  */
77
77
  baseRoute?: string;
78
+ /**
79
+ * Configuration to enable CORS (Cross-Origin Resource Sharing) in Fastay.
80
+ */
81
+ enableCors?: {
82
+ /**
83
+ * If true, permite requisições de qualquer origem.
84
+ * Default: false
85
+ */
86
+ allowAnyOrigin?: boolean;
87
+ /**
88
+ * Lista de origens específicas permitidas para envio de cookies.
89
+ * Exemplo: ["http://localhost:3000", "https://meusite.com"]
90
+ */
91
+ cookieOrigins?: string[];
92
+ /**
93
+ * Se true, habilita envio de cookies cross-origin.
94
+ * Default: false
95
+ */
96
+ credentials?: boolean;
97
+ /**
98
+ * Lista de métodos HTTP permitidos, separados por vírgula.
99
+ * Default: "GET,POST,PUT,PATCH,DELETE,OPTIONS"
100
+ */
101
+ methods?: string;
102
+ /**
103
+ * Lista de cabeçalhos permitidos na requisição.
104
+ * Default: "Content-Type, Authorization"
105
+ */
106
+ headers?: string;
107
+ /**
108
+ * Cabeçalhos expostos ao cliente.
109
+ * Exemplo: ["X-Custom-Header"]
110
+ */
111
+ exposedHeaders?: string;
112
+ /**
113
+ * Tempo máximo de cache para requisições prévias (preflight), em segundos.
114
+ */
115
+ maxAge?: number;
116
+ };
78
117
  /**
79
118
  * Port on which `.listen()` will run the server.
80
119
  * Default: 3000
package/dist/app.js CHANGED
@@ -5,6 +5,7 @@ import { loadFastayMiddlewares, createMiddleware, } from './middleware.js';
5
5
  import { logger } from './logger.js';
6
6
  import { printBanner } from './banner.js';
7
7
  import { RequestCookies } from './utils/cookies.js';
8
+ import { formDataMiddleware } from './utils/formDataMiddleware.js';
8
9
  /**
9
10
  * Bootstraps and configures a Fastay application.
10
11
  *
@@ -81,6 +82,8 @@ export async function createApp(opts) {
81
82
  app.use(mw);
82
83
  }
83
84
  }
85
+ // FormData middleware
86
+ app.use(formDataMiddleware());
84
87
  // Fastay middlewares
85
88
  if (opts?.middlewares) {
86
89
  logger.group('Fastay Middlewares');
@@ -95,6 +98,34 @@ export async function createApp(opts) {
95
98
  app.use((req, res, next) => {
96
99
  res.setHeader('X-Powered-By', 'Syntay Engine');
97
100
  req.cookies = new RequestCookies(req.headers.cookie);
101
+ const corsOpts = opts?.enableCors || {};
102
+ // Determina a origem
103
+ let origin = '*';
104
+ if (corsOpts.credentials && corsOpts.cookieOrigins?.length) {
105
+ // Se a origem estiver na lista de cookieOrigins, permite cookies
106
+ if (req.headers.origin &&
107
+ corsOpts.cookieOrigins.includes(req.headers.origin)) {
108
+ origin = req.headers.origin;
109
+ }
110
+ else {
111
+ origin = ''; // bloqueia cookies para outras origens
112
+ }
113
+ }
114
+ else if (!corsOpts.credentials && corsOpts.allowAnyOrigin) {
115
+ origin = '*';
116
+ }
117
+ res.setHeader('Access-Control-Allow-Origin', origin);
118
+ res.setHeader('Access-Control-Allow-Credentials', corsOpts.credentials ? 'true' : 'false');
119
+ res.setHeader('Access-Control-Allow-Methods', corsOpts.methods || 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
120
+ res.setHeader('Access-Control-Allow-Headers', corsOpts.headers || 'Content-Type, Authorization');
121
+ if (corsOpts.exposedHeaders) {
122
+ res.setHeader('Access-Control-Expose-Headers', corsOpts.exposedHeaders);
123
+ }
124
+ if (corsOpts.maxAge) {
125
+ res.setHeader('Access-Control-Max-Age', corsOpts.maxAge.toString());
126
+ }
127
+ if (req.method === 'OPTIONS')
128
+ return res.sendStatus(204);
98
129
  next();
99
130
  });
100
131
  // load routes
package/dist/index.d.ts CHANGED
@@ -2,3 +2,4 @@ export { createApp } from './app.js';
2
2
  export { createMiddleware } from './middleware.js';
3
3
  export type { CreateAppOptions } from './app.js';
4
4
  export type { Request, Response, Next } from './types';
5
+ export { cookies } from './utils/cookies.js';
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { createApp } from './app.js';
2
2
  export { createMiddleware } from './middleware.js';
3
+ export { cookies } from './utils/cookies.js';
@@ -26,6 +26,7 @@ export interface Request extends ExpressRequest {
26
26
  * Represents the cookies sent in a request.
27
27
  */
28
28
  cookies: RequestCookies;
29
+ formData: () => Promise<FormData>;
29
30
  }
30
31
  export type Response = ExpressResponse;
31
32
  export type Next = NextFunction;
@@ -7,3 +7,4 @@ export declare class RequestCookies {
7
7
  has(name: string): boolean;
8
8
  all(): Record<string, string>;
9
9
  }
10
+ export declare const cookies: typeof RequestCookies;
@@ -25,3 +25,4 @@ export class RequestCookies {
25
25
  return obj;
26
26
  }
27
27
  }
28
+ export const cookies = RequestCookies;
@@ -0,0 +1,17 @@
1
+ type FileInfo = {
2
+ filename: string;
3
+ encoding: string;
4
+ mimeType: string;
5
+ size: number;
6
+ buffer: Buffer;
7
+ };
8
+ export declare class File {
9
+ name: string;
10
+ type: string;
11
+ size: number;
12
+ buffer: Buffer;
13
+ constructor(file: FileInfo);
14
+ arrayBuffer(): ArrayBuffer | SharedArrayBuffer;
15
+ }
16
+ export declare function formDataMiddleware(): (req: any, res: any, next: any) => any;
17
+ export {};
@@ -0,0 +1,122 @@
1
+ import Busboy from 'busboy';
2
+ // Classe File compatível com a do browser
3
+ export class File {
4
+ constructor(file) {
5
+ this.name = file.filename;
6
+ this.type = file.mimeType;
7
+ this.size = file.size;
8
+ this.buffer = file.buffer;
9
+ }
10
+ arrayBuffer() {
11
+ return this.buffer.buffer.slice(this.buffer.byteOffset, this.buffer.byteOffset + this.buffer.byteLength);
12
+ }
13
+ }
14
+ export function formDataMiddleware() {
15
+ return function (req, res, next) {
16
+ if (typeof req.formData === 'function')
17
+ return next();
18
+ req.formData = () => new Promise((resolve, reject) => {
19
+ const bb = Busboy({ headers: req.headers });
20
+ const fields = {};
21
+ const files = {};
22
+ bb.on('field', (name, value) => {
23
+ if (name.endsWith('[]')) {
24
+ const key = name.slice(0, -2);
25
+ if (!fields[key])
26
+ fields[key] = [];
27
+ fields[key].push(value);
28
+ }
29
+ else {
30
+ fields[name] = value;
31
+ }
32
+ });
33
+ bb.on('file', (name, file, info) => {
34
+ const chunks = [];
35
+ file.on('data', (chunk) => chunks.push(chunk));
36
+ file.on('end', () => {
37
+ const buffer = Buffer.concat(chunks);
38
+ const fileObj = new File({
39
+ filename: info.filename,
40
+ encoding: info.encoding,
41
+ mimeType: info.mimeType,
42
+ size: buffer.length,
43
+ buffer,
44
+ });
45
+ if (!files[name])
46
+ files[name] = [];
47
+ files[name].push(fileObj);
48
+ });
49
+ });
50
+ bb.on('finish', () => {
51
+ resolve(createFormDataLike(fields, files));
52
+ });
53
+ bb.on('error', reject);
54
+ req.pipe(bb);
55
+ });
56
+ next();
57
+ };
58
+ }
59
+ function createFormDataLike(fields, files) {
60
+ return {
61
+ get(key) {
62
+ if (files[key])
63
+ return files[key][0];
64
+ return fields[key] ?? null;
65
+ },
66
+ getAll(key) {
67
+ if (files[key])
68
+ return files[key];
69
+ const val = fields[key];
70
+ return Array.isArray(val) ? val : val ? [val] : [];
71
+ },
72
+ has(key) {
73
+ return !!fields[key] || !!files[key];
74
+ },
75
+ append(key, value) {
76
+ if (value instanceof File) {
77
+ if (!files[key])
78
+ files[key] = [];
79
+ files[key].push(value);
80
+ return;
81
+ }
82
+ if (!fields[key]) {
83
+ fields[key] = value;
84
+ }
85
+ else if (Array.isArray(fields[key])) {
86
+ fields[key].push(value);
87
+ }
88
+ else {
89
+ fields[key] = [fields[key], value];
90
+ }
91
+ },
92
+ set(key, value) {
93
+ if (value instanceof File) {
94
+ files[key] = [value];
95
+ delete fields[key];
96
+ return;
97
+ }
98
+ fields[key] = value;
99
+ delete files[key];
100
+ },
101
+ delete(key) {
102
+ delete fields[key];
103
+ delete files[key];
104
+ },
105
+ *entries() {
106
+ for (const [k, v] of Object.entries(fields)) {
107
+ if (Array.isArray(v)) {
108
+ for (const item of v)
109
+ yield [k, item];
110
+ }
111
+ else {
112
+ yield [k, v];
113
+ }
114
+ }
115
+ for (const [k, arr] of Object.entries(files)) {
116
+ for (const file of arr)
117
+ yield [k, file];
118
+ }
119
+ },
120
+ raw: { fields, files },
121
+ };
122
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syntay/fastay",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Framework backend moderno baseado em Express.js, para criar APIs rapidamente",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -28,11 +28,15 @@
28
28
  "access": "public"
29
29
  },
30
30
  "dependencies": {
31
+ "busboy": "^1.6.0",
32
+ "chokidar": "^4.0.3",
31
33
  "express": "^5.1.0",
34
+ "import-fresh": "^3.3.1",
32
35
  "pino": "^10.1.0",
33
36
  "pino-pretty": "^13.1.2"
34
37
  },
35
38
  "devDependencies": {
39
+ "@types/busboy": "^1.5.4",
36
40
  "@types/express": "^5.0.5",
37
41
  "@types/node": "^20.19.25",
38
42
  "ts-node": "^10.9.2",
package/src/app.ts CHANGED
@@ -1,4 +1,4 @@
1
- import express from 'express';
1
+ import express, { Request, Response, NextFunction } from 'express';
2
2
  import path from 'path';
3
3
  import { loadApiRoutes } from './router.js';
4
4
  import {
@@ -9,8 +9,9 @@ import {
9
9
  import { logger } from './logger.js';
10
10
  import { printBanner } from './banner.js';
11
11
  import type { ServeStaticOptions } from 'serve-static';
12
- import { Next, Request, Response } from './types/index.js';
12
+
13
13
  import { RequestCookies } from './utils/cookies.js';
14
+ import { formDataMiddleware } from './utils/formDataMiddleware.js';
14
15
 
15
16
  /**
16
17
  * Express configuration options applied automatically by Fastay
@@ -96,6 +97,52 @@ export type CreateAppOptions = {
96
97
  */
97
98
  baseRoute?: string;
98
99
 
100
+ /**
101
+ * Configuration to enable CORS (Cross-Origin Resource Sharing) in Fastay.
102
+ */
103
+ enableCors?: {
104
+ /**
105
+ * If true, permite requisições de qualquer origem.
106
+ * Default: false
107
+ */
108
+ allowAnyOrigin?: boolean;
109
+
110
+ /**
111
+ * Lista de origens específicas permitidas para envio de cookies.
112
+ * Exemplo: ["http://localhost:3000", "https://meusite.com"]
113
+ */
114
+ cookieOrigins?: string[];
115
+
116
+ /**
117
+ * Se true, habilita envio de cookies cross-origin.
118
+ * Default: false
119
+ */
120
+ credentials?: boolean;
121
+
122
+ /**
123
+ * Lista de métodos HTTP permitidos, separados por vírgula.
124
+ * Default: "GET,POST,PUT,PATCH,DELETE,OPTIONS"
125
+ */
126
+ methods?: string;
127
+
128
+ /**
129
+ * Lista de cabeçalhos permitidos na requisição.
130
+ * Default: "Content-Type, Authorization"
131
+ */
132
+ headers?: string;
133
+
134
+ /**
135
+ * Cabeçalhos expostos ao cliente.
136
+ * Exemplo: ["X-Custom-Header"]
137
+ */
138
+ exposedHeaders?: string;
139
+
140
+ /**
141
+ * Tempo máximo de cache para requisições prévias (preflight), em segundos.
142
+ */
143
+ maxAge?: number;
144
+ };
145
+
99
146
  /**
100
147
  * Port on which `.listen()` will run the server.
101
148
  * Default: 3000
@@ -202,6 +249,9 @@ export async function createApp(opts?: CreateAppOptions) {
202
249
  }
203
250
  }
204
251
 
252
+ // FormData middleware
253
+ app.use(formDataMiddleware());
254
+
205
255
  // Fastay middlewares
206
256
  if (opts?.middlewares) {
207
257
  logger.group('Fastay Middlewares');
@@ -215,9 +265,52 @@ export async function createApp(opts?: CreateAppOptions) {
215
265
 
216
266
  // health check
217
267
  app.get('/_health', (_, res) => res.json({ ok: true }));
218
- app.use((req: Request, res: Response, next: Next) => {
268
+ app.use((req: Request, res: Response, next: NextFunction) => {
219
269
  res.setHeader('X-Powered-By', 'Syntay Engine');
220
270
  (req as any).cookies = new RequestCookies(req.headers.cookie);
271
+
272
+ const corsOpts = opts?.enableCors || {};
273
+
274
+ // Determina a origem
275
+ let origin = '*';
276
+
277
+ if (corsOpts.credentials && corsOpts.cookieOrigins?.length) {
278
+ // Se a origem estiver na lista de cookieOrigins, permite cookies
279
+ if (
280
+ req.headers.origin &&
281
+ corsOpts.cookieOrigins.includes(req.headers.origin)
282
+ ) {
283
+ origin = req.headers.origin;
284
+ } else {
285
+ origin = ''; // bloqueia cookies para outras origens
286
+ }
287
+ } else if (!corsOpts.credentials && corsOpts.allowAnyOrigin) {
288
+ origin = '*';
289
+ }
290
+
291
+ res.setHeader('Access-Control-Allow-Origin', origin);
292
+ res.setHeader(
293
+ 'Access-Control-Allow-Credentials',
294
+ corsOpts.credentials ? 'true' : 'false'
295
+ );
296
+ res.setHeader(
297
+ 'Access-Control-Allow-Methods',
298
+ corsOpts.methods || 'GET,POST,PUT,PATCH,DELETE,OPTIONS'
299
+ );
300
+ res.setHeader(
301
+ 'Access-Control-Allow-Headers',
302
+ corsOpts.headers || 'Content-Type, Authorization'
303
+ );
304
+
305
+ if (corsOpts.exposedHeaders) {
306
+ res.setHeader('Access-Control-Expose-Headers', corsOpts.exposedHeaders);
307
+ }
308
+
309
+ if (corsOpts.maxAge) {
310
+ res.setHeader('Access-Control-Max-Age', corsOpts.maxAge.toString());
311
+ }
312
+
313
+ if (req.method === 'OPTIONS') return res.sendStatus(204);
221
314
  next();
222
315
  });
223
316
 
package/src/index.ts CHANGED
@@ -2,3 +2,4 @@ export { createApp } from './app.js';
2
2
  export { createMiddleware } from './middleware.js';
3
3
  export type { CreateAppOptions } from './app.js';
4
4
  export type { Request, Response, Next } from './types';
5
+ export { cookies } from './utils/cookies.js';
@@ -35,6 +35,7 @@ export interface Request extends ExpressRequest {
35
35
  * Represents the cookies sent in a request.
36
36
  */
37
37
  cookies: RequestCookies;
38
+ formData: () => Promise<FormData>;
38
39
  // params:
39
40
  // query:
40
41
  }
@@ -30,3 +30,5 @@ export class RequestCookies {
30
30
  return obj;
31
31
  }
32
32
  }
33
+
34
+ export const cookies = RequestCookies;
@@ -0,0 +1,155 @@
1
+ import Busboy from 'busboy';
2
+
3
+ type FieldMap = Record<string, string | string[]>;
4
+ type FileInfo = {
5
+ filename: string;
6
+ encoding: string;
7
+ mimeType: string;
8
+ size: number;
9
+ buffer: Buffer;
10
+ };
11
+ type FileMap = Record<string, File[]>;
12
+
13
+ // Classe File compatível com a do browser
14
+ export class File {
15
+ name: string;
16
+ type: string;
17
+ size: number;
18
+ buffer: Buffer;
19
+
20
+ constructor(file: FileInfo) {
21
+ this.name = file.filename;
22
+ this.type = file.mimeType;
23
+ this.size = file.size;
24
+ this.buffer = file.buffer;
25
+ }
26
+
27
+ arrayBuffer() {
28
+ return this.buffer.buffer.slice(
29
+ this.buffer.byteOffset,
30
+ this.buffer.byteOffset + this.buffer.byteLength
31
+ );
32
+ }
33
+ }
34
+
35
+ export function formDataMiddleware() {
36
+ return function (req: any, res: any, next: any) {
37
+ if (typeof req.formData === 'function') return next();
38
+
39
+ req.formData = () =>
40
+ new Promise((resolve, reject) => {
41
+ const bb = Busboy({ headers: req.headers });
42
+
43
+ const fields: FieldMap = {};
44
+ const files: FileMap = {};
45
+
46
+ bb.on('field', (name, value) => {
47
+ if (name.endsWith('[]')) {
48
+ const key = name.slice(0, -2);
49
+ if (!fields[key]) fields[key] = [];
50
+ (fields[key] as string[]).push(value);
51
+ } else {
52
+ fields[name] = value;
53
+ }
54
+ });
55
+
56
+ bb.on('file', (name, file, info) => {
57
+ const chunks: Buffer[] = [];
58
+
59
+ file.on('data', (chunk) => chunks.push(chunk));
60
+
61
+ file.on('end', () => {
62
+ const buffer = Buffer.concat(chunks);
63
+
64
+ const fileObj = new File({
65
+ filename: info.filename,
66
+ encoding: info.encoding,
67
+ mimeType: info.mimeType,
68
+ size: buffer.length,
69
+ buffer,
70
+ });
71
+
72
+ if (!files[name]) files[name] = [];
73
+ files[name].push(fileObj);
74
+ });
75
+ });
76
+
77
+ bb.on('finish', () => {
78
+ resolve(createFormDataLike(fields, files));
79
+ });
80
+
81
+ bb.on('error', reject);
82
+
83
+ req.pipe(bb);
84
+ });
85
+
86
+ next();
87
+ };
88
+ }
89
+
90
+ function createFormDataLike(fields: FieldMap, files: FileMap) {
91
+ return {
92
+ get(key: string) {
93
+ if (files[key]) return files[key][0];
94
+ return fields[key] ?? null;
95
+ },
96
+
97
+ getAll(key: string) {
98
+ if (files[key]) return files[key];
99
+ const val = fields[key];
100
+ return Array.isArray(val) ? val : val ? [val] : [];
101
+ },
102
+
103
+ has(key: string) {
104
+ return !!fields[key] || !!files[key];
105
+ },
106
+
107
+ append(key: string, value: any) {
108
+ if (value instanceof File) {
109
+ if (!files[key]) files[key] = [];
110
+ files[key].push(value);
111
+ return;
112
+ }
113
+
114
+ if (!fields[key]) {
115
+ fields[key] = value;
116
+ } else if (Array.isArray(fields[key])) {
117
+ (fields[key] as string[]).push(value);
118
+ } else {
119
+ fields[key] = [fields[key] as string, value];
120
+ }
121
+ },
122
+
123
+ set(key: string, value: any) {
124
+ if (value instanceof File) {
125
+ files[key] = [value];
126
+ delete fields[key];
127
+ return;
128
+ }
129
+
130
+ fields[key] = value;
131
+ delete files[key];
132
+ },
133
+
134
+ delete(key: string) {
135
+ delete fields[key];
136
+ delete files[key];
137
+ },
138
+
139
+ *entries() {
140
+ for (const [k, v] of Object.entries(fields)) {
141
+ if (Array.isArray(v)) {
142
+ for (const item of v) yield [k, item];
143
+ } else {
144
+ yield [k, v];
145
+ }
146
+ }
147
+
148
+ for (const [k, arr] of Object.entries(files)) {
149
+ for (const file of arr) yield [k, file];
150
+ }
151
+ },
152
+
153
+ raw: { fields, files },
154
+ };
155
+ }