@buenojs/bueno 0.8.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.
Files changed (120) hide show
  1. package/.env.example +109 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/LICENSE +21 -0
  4. package/README.md +892 -0
  5. package/architecture.md +652 -0
  6. package/bun.lock +70 -0
  7. package/dist/cli/index.js +3233 -0
  8. package/dist/index.js +9014 -0
  9. package/package.json +77 -0
  10. package/src/cache/index.ts +795 -0
  11. package/src/cli/ARCHITECTURE.md +837 -0
  12. package/src/cli/bin.ts +10 -0
  13. package/src/cli/commands/build.ts +425 -0
  14. package/src/cli/commands/dev.ts +248 -0
  15. package/src/cli/commands/generate.ts +541 -0
  16. package/src/cli/commands/help.ts +55 -0
  17. package/src/cli/commands/index.ts +112 -0
  18. package/src/cli/commands/migration.ts +355 -0
  19. package/src/cli/commands/new.ts +804 -0
  20. package/src/cli/commands/start.ts +208 -0
  21. package/src/cli/core/args.ts +283 -0
  22. package/src/cli/core/console.ts +349 -0
  23. package/src/cli/core/index.ts +60 -0
  24. package/src/cli/core/prompt.ts +424 -0
  25. package/src/cli/core/spinner.ts +265 -0
  26. package/src/cli/index.ts +135 -0
  27. package/src/cli/templates/deploy.ts +295 -0
  28. package/src/cli/templates/docker.ts +307 -0
  29. package/src/cli/templates/index.ts +24 -0
  30. package/src/cli/utils/fs.ts +428 -0
  31. package/src/cli/utils/index.ts +8 -0
  32. package/src/cli/utils/strings.ts +197 -0
  33. package/src/config/env.ts +408 -0
  34. package/src/config/index.ts +506 -0
  35. package/src/config/loader.ts +329 -0
  36. package/src/config/merge.ts +285 -0
  37. package/src/config/types.ts +320 -0
  38. package/src/config/validation.ts +441 -0
  39. package/src/container/forward-ref.ts +143 -0
  40. package/src/container/index.ts +386 -0
  41. package/src/context/index.ts +360 -0
  42. package/src/database/index.ts +1142 -0
  43. package/src/database/migrations/index.ts +371 -0
  44. package/src/database/schema/index.ts +619 -0
  45. package/src/frontend/api-routes.ts +640 -0
  46. package/src/frontend/bundler.ts +643 -0
  47. package/src/frontend/console-client.ts +419 -0
  48. package/src/frontend/console-stream.ts +587 -0
  49. package/src/frontend/dev-server.ts +846 -0
  50. package/src/frontend/file-router.ts +611 -0
  51. package/src/frontend/frameworks/index.ts +106 -0
  52. package/src/frontend/frameworks/react.ts +85 -0
  53. package/src/frontend/frameworks/solid.ts +104 -0
  54. package/src/frontend/frameworks/svelte.ts +110 -0
  55. package/src/frontend/frameworks/vue.ts +92 -0
  56. package/src/frontend/hmr-client.ts +663 -0
  57. package/src/frontend/hmr.ts +728 -0
  58. package/src/frontend/index.ts +342 -0
  59. package/src/frontend/islands.ts +552 -0
  60. package/src/frontend/isr.ts +555 -0
  61. package/src/frontend/layout.ts +475 -0
  62. package/src/frontend/ssr/react.ts +446 -0
  63. package/src/frontend/ssr/solid.ts +523 -0
  64. package/src/frontend/ssr/svelte.ts +546 -0
  65. package/src/frontend/ssr/vue.ts +504 -0
  66. package/src/frontend/ssr.ts +699 -0
  67. package/src/frontend/types.ts +2274 -0
  68. package/src/health/index.ts +604 -0
  69. package/src/index.ts +410 -0
  70. package/src/lock/index.ts +587 -0
  71. package/src/logger/index.ts +444 -0
  72. package/src/logger/transports/index.ts +969 -0
  73. package/src/metrics/index.ts +494 -0
  74. package/src/middleware/built-in.ts +360 -0
  75. package/src/middleware/index.ts +94 -0
  76. package/src/modules/filters.ts +458 -0
  77. package/src/modules/guards.ts +405 -0
  78. package/src/modules/index.ts +1256 -0
  79. package/src/modules/interceptors.ts +574 -0
  80. package/src/modules/lazy.ts +418 -0
  81. package/src/modules/lifecycle.ts +478 -0
  82. package/src/modules/metadata.ts +90 -0
  83. package/src/modules/pipes.ts +626 -0
  84. package/src/router/index.ts +339 -0
  85. package/src/router/linear.ts +371 -0
  86. package/src/router/regex.ts +292 -0
  87. package/src/router/tree.ts +562 -0
  88. package/src/rpc/index.ts +1263 -0
  89. package/src/security/index.ts +436 -0
  90. package/src/ssg/index.ts +631 -0
  91. package/src/storage/index.ts +456 -0
  92. package/src/telemetry/index.ts +1097 -0
  93. package/src/testing/index.ts +1586 -0
  94. package/src/types/index.ts +236 -0
  95. package/src/types/optional-deps.d.ts +219 -0
  96. package/src/validation/index.ts +276 -0
  97. package/src/websocket/index.ts +1004 -0
  98. package/tests/integration/cli.test.ts +1016 -0
  99. package/tests/integration/fullstack.test.ts +234 -0
  100. package/tests/unit/cache.test.ts +174 -0
  101. package/tests/unit/cli-commands.test.ts +892 -0
  102. package/tests/unit/cli.test.ts +1258 -0
  103. package/tests/unit/container.test.ts +279 -0
  104. package/tests/unit/context.test.ts +221 -0
  105. package/tests/unit/database.test.ts +183 -0
  106. package/tests/unit/linear-router.test.ts +280 -0
  107. package/tests/unit/lock.test.ts +336 -0
  108. package/tests/unit/middleware.test.ts +184 -0
  109. package/tests/unit/modules.test.ts +142 -0
  110. package/tests/unit/pubsub.test.ts +257 -0
  111. package/tests/unit/regex-router.test.ts +265 -0
  112. package/tests/unit/router.test.ts +373 -0
  113. package/tests/unit/rpc.test.ts +1248 -0
  114. package/tests/unit/security.test.ts +174 -0
  115. package/tests/unit/telemetry.test.ts +371 -0
  116. package/tests/unit/test-cache.test.ts +110 -0
  117. package/tests/unit/test-database.test.ts +282 -0
  118. package/tests/unit/tree-router.test.ts +325 -0
  119. package/tests/unit/validation.test.ts +794 -0
  120. package/tsconfig.json +27 -0
@@ -0,0 +1,279 @@
1
+ import { describe, test, expect, beforeEach } from 'bun:test';
2
+ import { Container, Token } from '../../src/container';
3
+
4
+ // Test fixtures
5
+ interface ILogger {
6
+ log(message: string): void;
7
+ }
8
+
9
+ interface IDatabase {
10
+ query(sql: string): unknown[];
11
+ }
12
+
13
+ interface IUserService {
14
+ getUser(id: string): { id: string; name: string };
15
+ }
16
+
17
+ class ConsoleLogger implements ILogger {
18
+ log(message: string): void {
19
+ console.log(message);
20
+ }
21
+ }
22
+
23
+ class MockDatabase implements IDatabase {
24
+ query(sql: string): unknown[] {
25
+ return [{ id: 1, name: 'test' }];
26
+ }
27
+ }
28
+
29
+ class UserService implements IUserService {
30
+ constructor(
31
+ private logger: ILogger,
32
+ private db: IDatabase
33
+ ) {}
34
+
35
+ getUser(id: string): { id: string; name: string } {
36
+ this.logger.log(`Getting user ${id}`);
37
+ const results = this.db.query('SELECT * FROM users WHERE id = ?');
38
+ return { id, name: 'John' };
39
+ }
40
+ }
41
+
42
+ // Token definitions
43
+ const LOGGER_TOKEN = Token<ILogger>('ILogger');
44
+ const DATABASE_TOKEN = Token<IDatabase>('IDatabase');
45
+ const USER_SERVICE_TOKEN = Token<IUserService>('IUserService');
46
+
47
+ describe('Container', () => {
48
+ let container: Container;
49
+
50
+ beforeEach(() => {
51
+ container = new Container();
52
+ });
53
+
54
+ describe('Registration', () => {
55
+ test('should register a provider with useClass', () => {
56
+ container.register({
57
+ token: LOGGER_TOKEN,
58
+ useClass: ConsoleLogger,
59
+ });
60
+
61
+ expect(container.has(LOGGER_TOKEN)).toBe(true);
62
+ });
63
+
64
+ test('should register a provider with useValue', () => {
65
+ const logger = new ConsoleLogger();
66
+ container.register({
67
+ token: LOGGER_TOKEN,
68
+ useValue: logger,
69
+ });
70
+
71
+ expect(container.has(LOGGER_TOKEN)).toBe(true);
72
+ });
73
+
74
+ test('should register a provider with useFactory', () => {
75
+ container.register({
76
+ token: LOGGER_TOKEN,
77
+ useFactory: () => new ConsoleLogger(),
78
+ });
79
+
80
+ expect(container.has(LOGGER_TOKEN)).toBe(true);
81
+ });
82
+
83
+ test('should throw when registering duplicate token', () => {
84
+ container.register({
85
+ token: LOGGER_TOKEN,
86
+ useClass: ConsoleLogger,
87
+ });
88
+
89
+ expect(() => {
90
+ container.register({
91
+ token: LOGGER_TOKEN,
92
+ useClass: ConsoleLogger,
93
+ });
94
+ }).toThrow();
95
+ });
96
+ });
97
+
98
+ describe('Resolution', () => {
99
+ test('should resolve useClass provider', () => {
100
+ container.register({
101
+ token: LOGGER_TOKEN,
102
+ useClass: ConsoleLogger,
103
+ });
104
+
105
+ const logger = container.resolve<ILogger>(LOGGER_TOKEN);
106
+ expect(logger).toBeInstanceOf(ConsoleLogger);
107
+ });
108
+
109
+ test('should resolve useValue provider', () => {
110
+ const logger = new ConsoleLogger();
111
+ container.register({
112
+ token: LOGGER_TOKEN,
113
+ useValue: logger,
114
+ });
115
+
116
+ const resolved = container.resolve<ILogger>(LOGGER_TOKEN);
117
+ expect(resolved).toBe(logger);
118
+ });
119
+
120
+ test('should resolve useFactory provider', () => {
121
+ container.register({
122
+ token: LOGGER_TOKEN,
123
+ useFactory: () => new ConsoleLogger(),
124
+ });
125
+
126
+ const logger = container.resolve<ILogger>(LOGGER_TOKEN);
127
+ expect(logger).toBeInstanceOf(ConsoleLogger);
128
+ });
129
+
130
+ test('should throw when resolving unregistered token', () => {
131
+ expect(() => {
132
+ container.resolve(LOGGER_TOKEN);
133
+ }).toThrow();
134
+ });
135
+
136
+ test('should resolve dependencies via constructor injection', () => {
137
+ container.register({
138
+ token: LOGGER_TOKEN,
139
+ useClass: ConsoleLogger,
140
+ });
141
+
142
+ container.register({
143
+ token: DATABASE_TOKEN,
144
+ useClass: MockDatabase,
145
+ });
146
+
147
+ container.register({
148
+ token: USER_SERVICE_TOKEN,
149
+ useClass: UserService,
150
+ inject: [LOGGER_TOKEN, DATABASE_TOKEN],
151
+ });
152
+
153
+ const userService = container.resolve<IUserService>(USER_SERVICE_TOKEN);
154
+ expect(userService).toBeInstanceOf(UserService);
155
+ expect(userService.getUser('1')).toEqual({ id: '1', name: 'John' });
156
+ });
157
+
158
+ test('should resolve factory with injected dependencies', () => {
159
+ container.register({
160
+ token: LOGGER_TOKEN,
161
+ useClass: ConsoleLogger,
162
+ });
163
+
164
+ container.register({
165
+ token: DATABASE_TOKEN,
166
+ useClass: MockDatabase,
167
+ });
168
+
169
+ container.register({
170
+ token: USER_SERVICE_TOKEN,
171
+ useFactory: (logger, db) => new UserService(logger, db),
172
+ inject: [LOGGER_TOKEN, DATABASE_TOKEN],
173
+ });
174
+
175
+ const userService = container.resolve<IUserService>(USER_SERVICE_TOKEN);
176
+ expect(userService).toBeInstanceOf(UserService);
177
+ });
178
+ });
179
+
180
+ describe('Scopes', () => {
181
+ test('singleton scope should return same instance', () => {
182
+ container.register({
183
+ token: LOGGER_TOKEN,
184
+ useClass: ConsoleLogger,
185
+ scope: 'singleton',
186
+ });
187
+
188
+ const logger1 = container.resolve<ILogger>(LOGGER_TOKEN);
189
+ const logger2 = container.resolve<ILogger>(LOGGER_TOKEN);
190
+
191
+ expect(logger1).toBe(logger2);
192
+ });
193
+
194
+ test('transient scope should return new instances', () => {
195
+ container.register({
196
+ token: LOGGER_TOKEN,
197
+ useClass: ConsoleLogger,
198
+ scope: 'transient',
199
+ });
200
+
201
+ const logger1 = container.resolve<ILogger>(LOGGER_TOKEN);
202
+ const logger2 = container.resolve<ILogger>(LOGGER_TOKEN);
203
+
204
+ expect(logger1).not.toBe(logger2);
205
+ });
206
+
207
+ test('default scope should be singleton', () => {
208
+ container.register({
209
+ token: LOGGER_TOKEN,
210
+ useClass: ConsoleLogger,
211
+ });
212
+
213
+ const logger1 = container.resolve<ILogger>(LOGGER_TOKEN);
214
+ const logger2 = container.resolve<ILogger>(LOGGER_TOKEN);
215
+
216
+ expect(logger1).toBe(logger2);
217
+ });
218
+ });
219
+
220
+ describe('Circular Dependencies', () => {
221
+ test('should detect circular dependencies', () => {
222
+ const TOKEN_A = Token('ServiceA');
223
+ const TOKEN_B = Token('ServiceB');
224
+
225
+ container.register({
226
+ token: TOKEN_A,
227
+ useFactory: () => ({ b: container.resolve(TOKEN_B) }),
228
+ });
229
+
230
+ container.register({
231
+ token: TOKEN_B,
232
+ useFactory: () => ({ a: container.resolve(TOKEN_A) }),
233
+ });
234
+
235
+ // This should either throw or handle gracefully
236
+ // For now, we expect it to detect the circular dependency
237
+ // Note: Real implementation should track resolution stack
238
+ });
239
+ });
240
+
241
+ describe('Bulk Registration', () => {
242
+ test('should register multiple providers', () => {
243
+ container.registerAll([
244
+ { token: LOGGER_TOKEN, useClass: ConsoleLogger },
245
+ { token: DATABASE_TOKEN, useClass: MockDatabase },
246
+ ]);
247
+
248
+ expect(container.has(LOGGER_TOKEN)).toBe(true);
249
+ expect(container.has(DATABASE_TOKEN)).toBe(true);
250
+ });
251
+ });
252
+
253
+ describe('Clear', () => {
254
+ test('should clear all registrations', () => {
255
+ container.register({
256
+ token: LOGGER_TOKEN,
257
+ useClass: ConsoleLogger,
258
+ });
259
+
260
+ container.clear();
261
+
262
+ expect(container.has(LOGGER_TOKEN)).toBe(false);
263
+ });
264
+ });
265
+ });
266
+
267
+ describe('Token', () => {
268
+ test('should create a token with description', () => {
269
+ const token = Token<string>('myString');
270
+ expect(token.description).toBe('myString');
271
+ });
272
+
273
+ test('should be usable as object key', () => {
274
+ const token = Token<string>('myString');
275
+ const map = new Map();
276
+ map.set(token, 'value');
277
+ expect(map.get(token)).toBe('value');
278
+ });
279
+ });
@@ -0,0 +1,221 @@
1
+ import { describe, test, expect, beforeEach } from 'bun:test';
2
+ import { Context } from '../../src/context';
3
+ import type { PathParams } from '../../src/types';
4
+
5
+ describe('Context', () => {
6
+ let mockRequest: Request;
7
+ let context: Context;
8
+
9
+ beforeEach(() => {
10
+ mockRequest = new Request('http://localhost:3000/users/123?page=1&limit=10', {
11
+ method: 'GET',
12
+ headers: {
13
+ 'Content-Type': 'application/json',
14
+ 'Authorization': 'Bearer token123',
15
+ },
16
+ });
17
+ const params: PathParams = { id: '123' };
18
+ context = new Context(mockRequest, params);
19
+ });
20
+
21
+ describe('Request Access', () => {
22
+ test('should provide access to raw request', () => {
23
+ expect(context.req).toBe(mockRequest);
24
+ });
25
+
26
+ test('should provide request method', () => {
27
+ expect(context.method).toBe('GET');
28
+ });
29
+
30
+ test('should provide request URL', () => {
31
+ expect(context.url).toBeInstanceOf(URL);
32
+ expect(context.url.pathname).toBe('/users/123');
33
+ });
34
+
35
+ test('should provide path name', () => {
36
+ expect(context.path).toBe('/users/123');
37
+ });
38
+ });
39
+
40
+ describe('Parameters', () => {
41
+ test('should provide path parameters', () => {
42
+ expect(context.params).toEqual({ id: '123' });
43
+ });
44
+
45
+ test('should provide query parameters', () => {
46
+ expect(context.query.page).toBe('1');
47
+ expect(context.query.limit).toBe('10');
48
+ });
49
+
50
+ test('should handle missing query parameters', () => {
51
+ expect(context.query.missing).toBeUndefined();
52
+ });
53
+ });
54
+
55
+ describe('Headers', () => {
56
+ test('should get header value', () => {
57
+ expect(context.getHeader('content-type')).toBe('application/json');
58
+ expect(context.getHeader('authorization')).toBe('Bearer token123');
59
+ });
60
+
61
+ test('should be case-insensitive for headers', () => {
62
+ expect(context.getHeader('Content-Type')).toBe('application/json');
63
+ expect(context.getHeader('CONTENT-TYPE')).toBe('application/json');
64
+ });
65
+
66
+ test('should return undefined for missing headers', () => {
67
+ expect(context.getHeader('x-custom')).toBeUndefined();
68
+ });
69
+ });
70
+
71
+ describe('Cookies', () => {
72
+ test('should parse cookies from header', () => {
73
+ const requestWithCookies = new Request('http://localhost:3000/', {
74
+ headers: {
75
+ Cookie: 'session=abc123; user=john',
76
+ },
77
+ });
78
+ const ctx = new Context(requestWithCookies, {});
79
+
80
+ expect(ctx.getCookie('session')).toBe('abc123');
81
+ expect(ctx.getCookie('user')).toBe('john');
82
+ });
83
+
84
+ test('should return undefined for missing cookie', () => {
85
+ expect(context.getCookie('session')).toBeUndefined();
86
+ });
87
+ });
88
+
89
+ describe('Body Parsing', () => {
90
+ test('should parse JSON body', async () => {
91
+ const request = new Request('http://localhost:3000/', {
92
+ method: 'POST',
93
+ headers: { 'Content-Type': 'application/json' },
94
+ body: JSON.stringify({ name: 'John', age: 30 }),
95
+ });
96
+ const ctx = new Context(request, {});
97
+
98
+ const body = await ctx.body();
99
+ expect(body).toEqual({ name: 'John', age: 30 });
100
+ });
101
+
102
+ test('should parse form data', async () => {
103
+ const request = new Request('http://localhost:3000/', {
104
+ method: 'POST',
105
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
106
+ body: 'name=John&age=30',
107
+ });
108
+ const ctx = new Context(request, {});
109
+
110
+ const body = await ctx.bodyFormData();
111
+ expect(body.get('name')).toBe('John');
112
+ expect(body.get('age')).toBe('30');
113
+ });
114
+
115
+ test('should get text body', async () => {
116
+ const request = new Request('http://localhost:3000/', {
117
+ method: 'POST',
118
+ body: 'plain text body',
119
+ });
120
+ const ctx = new Context(request, {});
121
+
122
+ const body = await ctx.bodyText();
123
+ expect(body).toBe('plain text body');
124
+ });
125
+
126
+ test('should cache parsed body', async () => {
127
+ const request = new Request('http://localhost:3000/', {
128
+ method: 'POST',
129
+ headers: { 'Content-Type': 'application/json' },
130
+ body: JSON.stringify({ name: 'John' }),
131
+ });
132
+ const ctx = new Context(request, {});
133
+
134
+ const body1 = await ctx.body();
135
+ const body2 = await ctx.body();
136
+
137
+ expect(body1).toBe(body2);
138
+ });
139
+ });
140
+
141
+ describe('Variable Storage', () => {
142
+ test('should set and get variables', () => {
143
+ context.set('user', { id: 1, name: 'John' });
144
+ expect(context.get('user')).toEqual({ id: 1, name: 'John' });
145
+ });
146
+
147
+ test('should return undefined for missing variables', () => {
148
+ expect(context.get('missing')).toBeUndefined();
149
+ });
150
+
151
+ test('should check if variable exists', () => {
152
+ context.set('user', { id: 1 });
153
+ expect(context.has('user')).toBe(true);
154
+ expect(context.has('missing')).toBe(false);
155
+ });
156
+ });
157
+
158
+ describe('Response Building', () => {
159
+ test('should create JSON response', () => {
160
+ const response = context.json({ message: 'success' });
161
+ expect(response).toBeInstanceOf(Response);
162
+ expect(response.headers.get('Content-Type')).toContain('application/json');
163
+ });
164
+
165
+ test('should create text response', () => {
166
+ const response = context.text('Hello World');
167
+ expect(response).toBeInstanceOf(Response);
168
+ });
169
+
170
+ test('should create HTML response', () => {
171
+ const response = context.html('<h1>Hello</h1>');
172
+ expect(response).toBeInstanceOf(Response);
173
+ expect(response.headers.get('Content-Type')).toContain('text/html');
174
+ });
175
+
176
+ test('should create redirect response', () => {
177
+ const response = context.redirect('/new-location');
178
+ expect(response.status).toBe(302);
179
+ expect(response.headers.get('Location')).toBe('/new-location');
180
+ });
181
+
182
+ test('should create redirect with custom status', () => {
183
+ const response = context.redirect('/new-location', 301);
184
+ expect(response.status).toBe(301);
185
+ });
186
+
187
+ test('should set status code', () => {
188
+ context.status(201);
189
+ const response = context.json({ created: true });
190
+ expect(response.status).toBe(201);
191
+ });
192
+
193
+ test('should set response headers', () => {
194
+ context.setHeader('X-Custom', 'value');
195
+ const response = context.json({});
196
+ expect(response.headers.get('X-Custom')).toBe('value');
197
+ });
198
+
199
+ test('should create 404 response', () => {
200
+ const response = context.notFound('Resource not found');
201
+ expect(response.status).toBe(404);
202
+ });
203
+
204
+ test('should create error response', () => {
205
+ const response = context.error('Something went wrong', 500);
206
+ expect(response.status).toBe(500);
207
+ });
208
+ });
209
+
210
+ describe('Response Chain', () => {
211
+ test('should support method chaining', () => {
212
+ const response = context
213
+ .status(201)
214
+ .setHeader('X-Created', 'true')
215
+ .json({ id: 1 });
216
+
217
+ expect(response.status).toBe(201);
218
+ expect(response.headers.get('X-Created')).toBe('true');
219
+ });
220
+ });
221
+ });
@@ -0,0 +1,183 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
2
+ import { Database, detectDriver, createConnection, QueryBuilder, table } from '../../src/database';
3
+
4
+ describe('Database', () => {
5
+ // Use SQLite for testing (no external dependencies)
6
+ const testDbPath = ':memory:';
7
+
8
+ describe('detectDriver', () => {
9
+ test('should detect postgresql from URL', () => {
10
+ expect(detectDriver('postgresql://user:pass@localhost/db')).toBe('postgresql');
11
+ });
12
+
13
+ test('should detect mysql from URL', () => {
14
+ expect(detectDriver('mysql://user:pass@localhost/db')).toBe('mysql');
15
+ });
16
+
17
+ test('should detect sqlite from file path', () => {
18
+ expect(detectDriver('./test.db')).toBe('sqlite');
19
+ expect(detectDriver('/path/to/test.db')).toBe('sqlite');
20
+ });
21
+
22
+ test('should detect sqlite from sqlite:// URL', () => {
23
+ expect(detectDriver('sqlite://./test.db')).toBe('sqlite');
24
+ });
25
+ });
26
+
27
+ describe('Database (SQLite)', () => {
28
+ let db: Database;
29
+
30
+ beforeEach(async () => {
31
+ db = new Database({ url: testDbPath });
32
+ await db.connect();
33
+
34
+ // Create test table using raw SQL
35
+ await db.raw(`
36
+ CREATE TABLE IF NOT EXISTS users (
37
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
38
+ name TEXT NOT NULL,
39
+ email TEXT UNIQUE
40
+ )
41
+ `);
42
+ });
43
+
44
+ afterEach(async () => {
45
+ await db.close();
46
+ });
47
+
48
+ test('should connect to database', () => {
49
+ expect(db.isConnected).toBe(true);
50
+ });
51
+
52
+ test('should execute queries with tagged template', async () => {
53
+ await db.raw("INSERT INTO users (name, email) VALUES ('John', 'john@example.com')");
54
+
55
+ const count = await db.queryOne<{ count: number }>`SELECT COUNT(*) as count FROM users`;
56
+ expect(count?.count).toBe(1);
57
+ });
58
+
59
+ test('should query multiple rows', async () => {
60
+ await db.raw("INSERT INTO users (name, email) VALUES ('John', 'john@example.com')");
61
+ await db.raw("INSERT INTO users (name, email) VALUES ('Jane', 'jane@example.com')");
62
+
63
+ const users = await db.query<{ id: number; name: string; email: string }>`SELECT * FROM users ORDER BY name`;
64
+
65
+ expect(users.length).toBe(2);
66
+ expect(users[0].name).toBe('Jane');
67
+ expect(users[1].name).toBe('John');
68
+ });
69
+
70
+ test('should query single row', async () => {
71
+ await db.raw("INSERT INTO users (name, email) VALUES ('John', 'john@example.com')");
72
+
73
+ const user = await db.queryOne<{ id: number; name: string; email: string }>`
74
+ SELECT * FROM users WHERE name = ${'John'}
75
+ `;
76
+
77
+ expect(user).not.toBeNull();
78
+ expect(user?.name).toBe('John');
79
+ });
80
+
81
+ test('should handle transactions', async () => {
82
+ await db.transaction(async (tx) => {
83
+ await tx.execute`INSERT INTO users (name, email) VALUES (${'John'}, ${'john@example.com'})`;
84
+ });
85
+
86
+ const count = await db.queryOne<{ count: number }>`SELECT COUNT(*) as count FROM users`;
87
+ expect(count?.count).toBe(1);
88
+ });
89
+
90
+ test('should rollback on error', async () => {
91
+ try {
92
+ await db.transaction(async (tx) => {
93
+ await tx.execute`INSERT INTO users (name, email) VALUES (${'John'}, ${'john@example.com'})`;
94
+ throw new Error('Test error');
95
+ });
96
+ } catch (e) {
97
+ // Expected
98
+ }
99
+
100
+ const count = await db.queryOne<{ count: number }>`SELECT COUNT(*) as count FROM users`;
101
+ expect(count?.count).toBe(0);
102
+ });
103
+ });
104
+
105
+ describe('QueryBuilder', () => {
106
+ let db: Database;
107
+ let users: QueryBuilder<{ id: number; name: string; email: string }>;
108
+
109
+ beforeEach(async () => {
110
+ db = new Database({ url: testDbPath });
111
+ await db.connect();
112
+
113
+ await db.raw(`
114
+ CREATE TABLE IF NOT EXISTS users (
115
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
116
+ name TEXT NOT NULL,
117
+ email TEXT UNIQUE
118
+ )
119
+ `);
120
+
121
+ users = table<{ id: number; name: string; email: string }>(db, 'users');
122
+ });
123
+
124
+ afterEach(async () => {
125
+ await db.close();
126
+ });
127
+
128
+ test('should insert and find by id', async () => {
129
+ const inserted = await users.insert({ name: 'John', email: 'john@example.com' });
130
+ expect(inserted.name).toBe('John');
131
+
132
+ const found = await users.findById(inserted.id);
133
+ expect(found?.name).toBe('John');
134
+ });
135
+
136
+ test('should count rows', async () => {
137
+ await users.insert({ name: 'John', email: 'john@example.com' });
138
+ await users.insert({ name: 'Jane', email: 'jane@example.com' });
139
+
140
+ const count = await users.count();
141
+ expect(count).toBe(2);
142
+ });
143
+
144
+ test('should delete by id', async () => {
145
+ const inserted = await users.insert({ name: 'John', email: 'john@example.com' });
146
+
147
+ const deleted = await users.deleteById(inserted.id);
148
+ expect(deleted).toBe(true);
149
+
150
+ const found = await users.findById(inserted.id);
151
+ expect(found).toBeNull();
152
+ });
153
+
154
+ test('should update by id', async () => {
155
+ const inserted = await users.insert({ name: 'John', email: 'john@example.com' });
156
+
157
+ const updated = await users.updateById(inserted.id, { name: 'Johnny' });
158
+ expect(updated?.name).toBe('Johnny');
159
+ });
160
+
161
+ test('should paginate results', async () => {
162
+ for (let i = 0; i < 25; i++) {
163
+ await users.insert({ name: `User${i}`, email: `user${i}@example.com` });
164
+ }
165
+
166
+ const page1 = await users.paginate(1, 10);
167
+ expect(page1.data.length).toBe(10);
168
+ expect(page1.total).toBe(25);
169
+ expect(page1.totalPages).toBe(3);
170
+
171
+ const page3 = await users.paginate(3, 10);
172
+ expect(page3.data.length).toBe(5);
173
+ });
174
+ });
175
+
176
+ describe('createConnection', () => {
177
+ test('should create and connect to database', async () => {
178
+ const db = await createConnection({ url: ':memory:' });
179
+ expect(db.isConnected).toBe(true);
180
+ await db.close();
181
+ });
182
+ });
183
+ });