@carno.js/core 1.0.2 → 1.0.4

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.
Files changed (37) hide show
  1. package/LICENSE +21 -674
  2. package/README.md +188 -188
  3. package/dist/bun/index.js +7 -20
  4. package/dist/bun/index.js.map +28 -28
  5. package/dist/context/Context.js +2 -1
  6. package/dist/context/Context.mjs +2 -1
  7. package/dist/utils/parseQuery.js +84 -0
  8. package/dist/utils/parseQuery.mjs +63 -0
  9. package/package.json +3 -2
  10. package/src/Carno.ts +605 -605
  11. package/src/DefaultRoutes.ts +34 -34
  12. package/src/cache/CacheDriver.ts +50 -50
  13. package/src/cache/CacheService.ts +139 -139
  14. package/src/cache/MemoryDriver.ts +104 -104
  15. package/src/cache/RedisDriver.ts +116 -116
  16. package/src/compiler/JITCompiler.ts +167 -167
  17. package/src/container/Container.ts +168 -168
  18. package/src/context/Context.ts +130 -128
  19. package/src/cors/CorsHandler.ts +145 -145
  20. package/src/decorators/Controller.ts +63 -63
  21. package/src/decorators/Inject.ts +16 -16
  22. package/src/decorators/Middleware.ts +22 -22
  23. package/src/decorators/Service.ts +18 -18
  24. package/src/decorators/methods.ts +58 -58
  25. package/src/decorators/params.ts +47 -47
  26. package/src/events/Lifecycle.ts +97 -97
  27. package/src/exceptions/HttpException.ts +99 -99
  28. package/src/index.ts +92 -92
  29. package/src/metadata.ts +46 -46
  30. package/src/middleware/CarnoMiddleware.ts +14 -14
  31. package/src/router/RadixRouter.ts +225 -225
  32. package/src/testing/TestHarness.ts +177 -177
  33. package/src/utils/Metadata.ts +43 -43
  34. package/src/utils/parseQuery.ts +161 -0
  35. package/src/validation/ValibotAdapter.ts +95 -95
  36. package/src/validation/ValidatorAdapter.ts +69 -69
  37. package/src/validation/ZodAdapter.ts +102 -102
@@ -1,177 +1,177 @@
1
- import type { Server } from 'bun';
2
- import { Carno, type CarnoConfig } from '../Carno';
3
- import { Container, type Token } from '../container/Container';
4
-
5
- /**
6
- * Test configuration options.
7
- */
8
- export interface TestOptions {
9
- config?: CarnoConfig;
10
- listen?: boolean | number;
11
- port?: number;
12
- controllers?: (new (...args: any[]) => any)[];
13
- services?: (Token | any)[];
14
- }
15
-
16
- /**
17
- * Test harness - provides utilities for testing Carno applications.
18
- */
19
- export interface TestHarness {
20
- /** The Carno app instance */
21
- app: Carno;
22
-
23
- /** The internal DI container */
24
- container: Container;
25
-
26
- /** The HTTP server (if listening) */
27
- server?: Server<any>;
28
-
29
- /** The port the server is running on */
30
- port?: number;
31
-
32
- /** Resolve a service from the container */
33
- resolve<T>(token: Token<T>): T;
34
-
35
- /** Make an HTTP request to the app */
36
- request(path: string, init?: RequestInit): Promise<Response>;
37
-
38
- /** Make a GET request */
39
- get(path: string, init?: Omit<RequestInit, 'method'>): Promise<Response>;
40
-
41
- /** Make a POST request */
42
- post(path: string, body?: any, init?: Omit<RequestInit, 'method' | 'body'>): Promise<Response>;
43
-
44
- /** Make a PUT request */
45
- put(path: string, body?: any, init?: Omit<RequestInit, 'method' | 'body'>): Promise<Response>;
46
-
47
- /** Make a DELETE request */
48
- delete(path: string, init?: Omit<RequestInit, 'method'>): Promise<Response>;
49
-
50
- /** Close the test harness and cleanup */
51
- close(): Promise<void>;
52
- }
53
-
54
- /**
55
- * Create a test harness for Turbo applications.
56
- *
57
- * @example
58
- * ```typescript
59
- * const harness = await createTestHarness({
60
- * controllers: [UserController],
61
- * services: [UserService],
62
- * listen: true
63
- * });
64
- *
65
- * const response = await harness.get('/users');
66
- * expect(response.status).toBe(200);
67
- *
68
- * await harness.close();
69
- * ```
70
- */
71
- export async function createTestHarness(options: TestOptions = {}): Promise<TestHarness> {
72
- const config: CarnoConfig = {
73
- ...options.config,
74
- disableStartupLog: true
75
- };
76
-
77
- const app = new Carno(config);
78
-
79
- // Register controllers
80
- if (options.controllers) {
81
- app.controllers(options.controllers);
82
- }
83
-
84
- // Register services
85
- if (options.services) {
86
- app.services(options.services);
87
- }
88
-
89
- const port = resolvePort(options);
90
- let server: Server<any> | undefined;
91
-
92
- if (shouldListen(options.listen)) {
93
- app.listen(port);
94
- server = (app as any).server;
95
- }
96
-
97
- const actualPort = server?.port ?? port;
98
- const container = (app as any).container as Container;
99
-
100
- // Pre-bind methods for performance
101
- const baseUrl = `http://127.0.0.1:${actualPort}`;
102
-
103
- const request = async (path: string, init?: RequestInit): Promise<Response> => {
104
- if (!server) {
105
- throw new Error('Server not running. Set listen: true in options.');
106
- }
107
- const url = path.startsWith('http') ? path : `${baseUrl}${path.startsWith('/') ? path : '/' + path}`;
108
- return fetch(url, init);
109
- };
110
-
111
- return {
112
- app,
113
- container,
114
- server,
115
- port: actualPort,
116
-
117
- resolve: <T>(token: Token<T>): T => container.get(token),
118
-
119
- request,
120
-
121
- get: (path, init) => request(path, { ...init, method: 'GET' }),
122
-
123
- post: (path, body, init) => request(path, {
124
- ...init,
125
- method: 'POST',
126
- body: body ? JSON.stringify(body) : undefined,
127
- headers: { 'Content-Type': 'application/json', ...init?.headers }
128
- }),
129
-
130
- put: (path, body, init) => request(path, {
131
- ...init,
132
- method: 'PUT',
133
- body: body ? JSON.stringify(body) : undefined,
134
- headers: { 'Content-Type': 'application/json', ...init?.headers }
135
- }),
136
-
137
- delete: (path, init) => request(path, { ...init, method: 'DELETE' }),
138
-
139
- close: async () => {
140
- app.stop();
141
- }
142
- };
143
- }
144
-
145
- /**
146
- * Run a test routine with automatic harness cleanup.
147
- *
148
- * @example
149
- * ```typescript
150
- * await withTestApp(async (harness) => {
151
- * const response = await harness.get('/health');
152
- * expect(response.status).toBe(200);
153
- * }, { controllers: [HealthController], listen: true });
154
- * ```
155
- */
156
- export async function withTestApp(
157
- routine: (harness: TestHarness) => Promise<void>,
158
- options: TestOptions = {}
159
- ): Promise<void> {
160
- const harness = await createTestHarness(options);
161
-
162
- try {
163
- await routine(harness);
164
- } finally {
165
- await harness.close();
166
- }
167
- }
168
-
169
- function shouldListen(value: TestOptions['listen']): boolean {
170
- return typeof value === 'number' || Boolean(value);
171
- }
172
-
173
- function resolvePort(options: TestOptions): number {
174
- if (typeof options.listen === 'number') return options.listen;
175
- if (typeof options.port === 'number') return options.port;
176
- return 0; // Random port
177
- }
1
+ import type { Server } from 'bun';
2
+ import { Carno, type CarnoConfig } from '../Carno';
3
+ import { Container, type Token } from '../container/Container';
4
+
5
+ /**
6
+ * Test configuration options.
7
+ */
8
+ export interface TestOptions {
9
+ config?: CarnoConfig;
10
+ listen?: boolean | number;
11
+ port?: number;
12
+ controllers?: (new (...args: any[]) => any)[];
13
+ services?: (Token | any)[];
14
+ }
15
+
16
+ /**
17
+ * Test harness - provides utilities for testing Carno applications.
18
+ */
19
+ export interface TestHarness {
20
+ /** The Carno app instance */
21
+ app: Carno;
22
+
23
+ /** The internal DI container */
24
+ container: Container;
25
+
26
+ /** The HTTP server (if listening) */
27
+ server?: Server<any>;
28
+
29
+ /** The port the server is running on */
30
+ port?: number;
31
+
32
+ /** Resolve a service from the container */
33
+ resolve<T>(token: Token<T>): T;
34
+
35
+ /** Make an HTTP request to the app */
36
+ request(path: string, init?: RequestInit): Promise<Response>;
37
+
38
+ /** Make a GET request */
39
+ get(path: string, init?: Omit<RequestInit, 'method'>): Promise<Response>;
40
+
41
+ /** Make a POST request */
42
+ post(path: string, body?: any, init?: Omit<RequestInit, 'method' | 'body'>): Promise<Response>;
43
+
44
+ /** Make a PUT request */
45
+ put(path: string, body?: any, init?: Omit<RequestInit, 'method' | 'body'>): Promise<Response>;
46
+
47
+ /** Make a DELETE request */
48
+ delete(path: string, init?: Omit<RequestInit, 'method'>): Promise<Response>;
49
+
50
+ /** Close the test harness and cleanup */
51
+ close(): Promise<void>;
52
+ }
53
+
54
+ /**
55
+ * Create a test harness for Turbo applications.
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * const harness = await createTestHarness({
60
+ * controllers: [UserController],
61
+ * services: [UserService],
62
+ * listen: true
63
+ * });
64
+ *
65
+ * const response = await harness.get('/users');
66
+ * expect(response.status).toBe(200);
67
+ *
68
+ * await harness.close();
69
+ * ```
70
+ */
71
+ export async function createTestHarness(options: TestOptions = {}): Promise<TestHarness> {
72
+ const config: CarnoConfig = {
73
+ ...options.config,
74
+ disableStartupLog: true
75
+ };
76
+
77
+ const app = new Carno(config);
78
+
79
+ // Register controllers
80
+ if (options.controllers) {
81
+ app.controllers(options.controllers);
82
+ }
83
+
84
+ // Register services
85
+ if (options.services) {
86
+ app.services(options.services);
87
+ }
88
+
89
+ const port = resolvePort(options);
90
+ let server: Server<any> | undefined;
91
+
92
+ if (shouldListen(options.listen)) {
93
+ app.listen(port);
94
+ server = (app as any).server;
95
+ }
96
+
97
+ const actualPort = server?.port ?? port;
98
+ const container = (app as any).container as Container;
99
+
100
+ // Pre-bind methods for performance
101
+ const baseUrl = `http://127.0.0.1:${actualPort}`;
102
+
103
+ const request = async (path: string, init?: RequestInit): Promise<Response> => {
104
+ if (!server) {
105
+ throw new Error('Server not running. Set listen: true in options.');
106
+ }
107
+ const url = path.startsWith('http') ? path : `${baseUrl}${path.startsWith('/') ? path : '/' + path}`;
108
+ return fetch(url, init);
109
+ };
110
+
111
+ return {
112
+ app,
113
+ container,
114
+ server,
115
+ port: actualPort,
116
+
117
+ resolve: <T>(token: Token<T>): T => container.get(token),
118
+
119
+ request,
120
+
121
+ get: (path, init) => request(path, { ...init, method: 'GET' }),
122
+
123
+ post: (path, body, init) => request(path, {
124
+ ...init,
125
+ method: 'POST',
126
+ body: body ? JSON.stringify(body) : undefined,
127
+ headers: { 'Content-Type': 'application/json', ...init?.headers }
128
+ }),
129
+
130
+ put: (path, body, init) => request(path, {
131
+ ...init,
132
+ method: 'PUT',
133
+ body: body ? JSON.stringify(body) : undefined,
134
+ headers: { 'Content-Type': 'application/json', ...init?.headers }
135
+ }),
136
+
137
+ delete: (path, init) => request(path, { ...init, method: 'DELETE' }),
138
+
139
+ close: async () => {
140
+ app.stop();
141
+ }
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Run a test routine with automatic harness cleanup.
147
+ *
148
+ * @example
149
+ * ```typescript
150
+ * await withTestApp(async (harness) => {
151
+ * const response = await harness.get('/health');
152
+ * expect(response.status).toBe(200);
153
+ * }, { controllers: [HealthController], listen: true });
154
+ * ```
155
+ */
156
+ export async function withTestApp(
157
+ routine: (harness: TestHarness) => Promise<void>,
158
+ options: TestOptions = {}
159
+ ): Promise<void> {
160
+ const harness = await createTestHarness(options);
161
+
162
+ try {
163
+ await routine(harness);
164
+ } finally {
165
+ await harness.close();
166
+ }
167
+ }
168
+
169
+ function shouldListen(value: TestOptions['listen']): boolean {
170
+ return typeof value === 'number' || Boolean(value);
171
+ }
172
+
173
+ function resolvePort(options: TestOptions): number {
174
+ if (typeof options.listen === 'number') return options.listen;
175
+ if (typeof options.port === 'number') return options.port;
176
+ return 0; // Random port
177
+ }
@@ -1,43 +1,43 @@
1
- /**
2
- * Utility class for handling metadata operations.
3
- * Wraps Reflect.getMetadata and Reflect.defineMetadata.
4
- */
5
- export class Metadata {
6
- static get<T = any>(key: string | symbol, target: any): T | undefined {
7
- return Reflect.getMetadata(key, target);
8
- }
9
-
10
- static set(key: string | symbol, value: any, target: any): void {
11
- Reflect.defineMetadata(key, value, target);
12
- }
13
-
14
- static has(key: string | symbol, target: any): boolean {
15
- return Reflect.hasMetadata(key, target);
16
- }
17
-
18
- static delete(key: string | symbol, target: any): boolean {
19
- return Reflect.deleteMetadata(key, target);
20
- }
21
-
22
- static keys(target: any): (string | symbol)[] {
23
- return Reflect.getMetadataKeys(target);
24
- }
25
-
26
- static getType(target: any, propertyKey: string | symbol): any {
27
- return Reflect.getMetadata('design:type', target, propertyKey);
28
- }
29
- }
30
-
31
- /**
32
- * Type guard for checking if value is an object.
33
- */
34
- export function isObject(value: unknown): value is Record<string, any> {
35
- return typeof value === 'object' && value !== null && !Array.isArray(value);
36
- }
37
-
38
- /**
39
- * Type guard for checking if value is a string.
40
- */
41
- export function isString(value: unknown): value is string {
42
- return typeof value === 'string';
43
- }
1
+ /**
2
+ * Utility class for handling metadata operations.
3
+ * Wraps Reflect.getMetadata and Reflect.defineMetadata.
4
+ */
5
+ export class Metadata {
6
+ static get<T = any>(key: string | symbol, target: any): T | undefined {
7
+ return Reflect.getMetadata(key, target);
8
+ }
9
+
10
+ static set(key: string | symbol, value: any, target: any): void {
11
+ Reflect.defineMetadata(key, value, target);
12
+ }
13
+
14
+ static has(key: string | symbol, target: any): boolean {
15
+ return Reflect.hasMetadata(key, target);
16
+ }
17
+
18
+ static delete(key: string | symbol, target: any): boolean {
19
+ return Reflect.deleteMetadata(key, target);
20
+ }
21
+
22
+ static keys(target: any): (string | symbol)[] {
23
+ return Reflect.getMetadataKeys(target);
24
+ }
25
+
26
+ static getType(target: any, propertyKey: string | symbol): any {
27
+ return Reflect.getMetadata('design:type', target, propertyKey);
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Type guard for checking if value is an object.
33
+ */
34
+ export function isObject(value: unknown): value is Record<string, any> {
35
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
36
+ }
37
+
38
+ /**
39
+ * Type guard for checking if value is a string.
40
+ */
41
+ export function isString(value: unknown): value is string {
42
+ return typeof value === 'string';
43
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * High-performance query string parser.
3
+ *
4
+ * Based on Elysia's approach - uses manual string parsing with charCodeAt()
5
+ * instead of new URL() for significant performance gains.
6
+ *
7
+ * Benchmark: ~10x faster than new URL().searchParams
8
+ */
9
+
10
+ // Bit flags for tracking decode requirements
11
+ const KEY_HAS_PLUS = 1;
12
+ const KEY_NEEDS_DECODE = 2;
13
+ const VALUE_HAS_PLUS = 4;
14
+ const VALUE_NEEDS_DECODE = 8;
15
+
16
+ /**
17
+ * Parse query string from a full URL.
18
+ * Extracts the query portion and parses key-value pairs.
19
+ *
20
+ * @param url Full URL string (e.g., "http://localhost/path?foo=bar&baz=123")
21
+ * @returns Record<string, string> - parsed query parameters
22
+ */
23
+ export function parseQueryFromURL(url: string): Record<string, string> {
24
+ // Find the start of query string
25
+ const queryStart = url.indexOf('?');
26
+
27
+ if (queryStart === -1) {
28
+ return Object.create(null);
29
+ }
30
+
31
+ // Find the end of query string (before hash if present)
32
+ let queryEnd = url.indexOf('#', queryStart);
33
+
34
+ if (queryEnd === -1) {
35
+ queryEnd = url.length;
36
+ }
37
+
38
+ return parseQuery(url, queryStart + 1, queryEnd);
39
+ }
40
+
41
+ /**
42
+ * Parse query string directly.
43
+ *
44
+ * @param input Query string without leading '?' (e.g., "foo=bar&baz=123")
45
+ * @returns Record<string, string> - parsed query parameters
46
+ */
47
+ export function parseQueryString(input: string): Record<string, string> {
48
+ return parseQuery(input, 0, input.length);
49
+ }
50
+
51
+ /**
52
+ * Internal parser - parses query string from startIndex to endIndex.
53
+ */
54
+ function parseQuery(
55
+ input: string,
56
+ startIndex: number,
57
+ endIndex: number
58
+ ): Record<string, string> {
59
+ const result: Record<string, string> = Object.create(null);
60
+
61
+ let flags = 0;
62
+ let startingIndex = startIndex - 1;
63
+ let equalityIndex = startingIndex;
64
+
65
+ for (let i = startIndex; i < endIndex; i++) {
66
+ switch (input.charCodeAt(i)) {
67
+ // '&' - separator between key-value pairs
68
+ case 38:
69
+ processKeyValuePair(i);
70
+ startingIndex = i;
71
+ equalityIndex = i;
72
+ flags = 0;
73
+ break;
74
+
75
+ // '=' - separator between key and value
76
+ case 61:
77
+ if (equalityIndex <= startingIndex) {
78
+ equalityIndex = i;
79
+ } else {
80
+ // Multiple '=' means value needs decode
81
+ flags |= VALUE_NEEDS_DECODE;
82
+ }
83
+ break;
84
+
85
+ // '+' - space encoding
86
+ case 43:
87
+ if (equalityIndex > startingIndex) {
88
+ flags |= VALUE_HAS_PLUS;
89
+ } else {
90
+ flags |= KEY_HAS_PLUS;
91
+ }
92
+ break;
93
+
94
+ // '%' - URL encoding
95
+ case 37:
96
+ if (equalityIndex > startingIndex) {
97
+ flags |= VALUE_NEEDS_DECODE;
98
+ } else {
99
+ flags |= KEY_NEEDS_DECODE;
100
+ }
101
+ break;
102
+ }
103
+ }
104
+
105
+ // Process the last pair
106
+ if (startingIndex < endIndex) {
107
+ processKeyValuePair(endIndex);
108
+ }
109
+
110
+ return result;
111
+
112
+ function processKeyValuePair(pairEndIndex: number) {
113
+ const hasBothKeyValuePair = equalityIndex > startingIndex;
114
+ const effectiveEqualityIndex = hasBothKeyValuePair
115
+ ? equalityIndex
116
+ : pairEndIndex;
117
+
118
+ const keySlice = input.slice(startingIndex + 1, effectiveEqualityIndex);
119
+
120
+ // Skip empty keys
121
+ if (!hasBothKeyValuePair && keySlice.length === 0) {
122
+ return;
123
+ }
124
+
125
+ let finalKey = keySlice;
126
+
127
+ if (flags & KEY_HAS_PLUS) {
128
+ finalKey = finalKey.replace(/\+/g, ' ');
129
+ }
130
+
131
+ if (flags & KEY_NEEDS_DECODE) {
132
+ try {
133
+ finalKey = decodeURIComponent(finalKey);
134
+ } catch {
135
+ // Keep original if decode fails
136
+ }
137
+ }
138
+
139
+ let finalValue = '';
140
+
141
+ if (hasBothKeyValuePair) {
142
+ let valueSlice = input.slice(equalityIndex + 1, pairEndIndex);
143
+
144
+ if (flags & VALUE_HAS_PLUS) {
145
+ valueSlice = valueSlice.replace(/\+/g, ' ');
146
+ }
147
+
148
+ if (flags & VALUE_NEEDS_DECODE) {
149
+ try {
150
+ finalValue = decodeURIComponent(valueSlice);
151
+ } catch {
152
+ finalValue = valueSlice;
153
+ }
154
+ } else {
155
+ finalValue = valueSlice;
156
+ }
157
+ }
158
+
159
+ result[finalKey] = finalValue;
160
+ }
161
+ }