triva 0.0.2 → 0.3.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.
@@ -0,0 +1,114 @@
1
+ /*!
2
+ * Triva - Cookie Parser
3
+ * Copyright (c) 2026 Kris Powers
4
+ * License MIT
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ /* ---------------- Cookie Parsing ---------------- */
10
+ function parseCookies(cookieHeader) {
11
+ if (!cookieHeader) return {};
12
+
13
+ const cookies = {};
14
+
15
+ cookieHeader.split(';').forEach(cookie => {
16
+ const parts = cookie.trim().split('=');
17
+ const key = parts[0];
18
+ const value = parts.slice(1).join('='); // Handle values with '=' in them
19
+
20
+ if (key) {
21
+ try {
22
+ // Decode URI components
23
+ cookies[key] = decodeURIComponent(value);
24
+ } catch (e) {
25
+ // If decode fails, use raw value
26
+ cookies[key] = value;
27
+ }
28
+ }
29
+ });
30
+
31
+ return cookies;
32
+ }
33
+
34
+ /* ---------------- Cookie Serialization ---------------- */
35
+ function serializeCookie(name, value, options = {}) {
36
+ let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
37
+
38
+ if (options.maxAge) {
39
+ cookie += `; Max-Age=${options.maxAge}`;
40
+ }
41
+
42
+ if (options.expires) {
43
+ const expires = options.expires instanceof Date
44
+ ? options.expires.toUTCString()
45
+ : new Date(options.expires).toUTCString();
46
+ cookie += `; Expires=${expires}`;
47
+ }
48
+
49
+ if (options.domain) {
50
+ cookie += `; Domain=${options.domain}`;
51
+ }
52
+
53
+ if (options.path) {
54
+ cookie += `; Path=${options.path}`;
55
+ } else {
56
+ cookie += `; Path=/`;
57
+ }
58
+
59
+ if (options.secure) {
60
+ cookie += `; Secure`;
61
+ }
62
+
63
+ if (options.httpOnly) {
64
+ cookie += `; HttpOnly`;
65
+ }
66
+
67
+ if (options.sameSite) {
68
+ const sameSite = typeof options.sameSite === 'string'
69
+ ? options.sameSite
70
+ : (options.sameSite === true ? 'Strict' : 'Lax');
71
+ cookie += `; SameSite=${sameSite}`;
72
+ }
73
+
74
+ return cookie;
75
+ }
76
+
77
+ /* ---------------- Cookie Parser Middleware ---------------- */
78
+ function cookieParser(secret) {
79
+ return (req, res, next) => {
80
+ // Parse cookies from request
81
+ const cookieHeader = req.headers.cookie;
82
+ req.cookies = parseCookies(cookieHeader);
83
+
84
+ // Add cookie helper methods to response
85
+ res.cookie = (name, value, options = {}) => {
86
+ const cookie = serializeCookie(name, value, options);
87
+
88
+ // Handle multiple Set-Cookie headers
89
+ const existing = res.getHeader('Set-Cookie');
90
+ if (existing) {
91
+ const cookies = Array.isArray(existing) ? existing : [existing];
92
+ cookies.push(cookie);
93
+ res.setHeader('Set-Cookie', cookies);
94
+ } else {
95
+ res.setHeader('Set-Cookie', cookie);
96
+ }
97
+
98
+ return res;
99
+ };
100
+
101
+ res.clearCookie = (name, options = {}) => {
102
+ const clearOptions = {
103
+ ...options,
104
+ expires: new Date(0),
105
+ maxAge: -1
106
+ };
107
+ return res.cookie(name, '', clearOptions);
108
+ };
109
+
110
+ next();
111
+ };
112
+ }
113
+
114
+ export { parseCookies, serializeCookie, cookieParser };
@@ -0,0 +1,580 @@
1
+ /*!
2
+ * Triva - Database Adapters
3
+ * Copyright (c) 2026 Kris Powers
4
+ * License MIT
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ /* ---------------- Base Adapter Interface ---------------- */
10
+ class DatabaseAdapter {
11
+ constructor(config) {
12
+ this.config = config;
13
+ this.connected = false;
14
+ }
15
+
16
+ async connect() {
17
+ throw new Error('connect() must be implemented');
18
+ }
19
+
20
+ async disconnect() {
21
+ throw new Error('disconnect() must be implemented');
22
+ }
23
+
24
+ async get(key) {
25
+ throw new Error('get() must be implemented');
26
+ }
27
+
28
+ async set(key, value, ttl = null) {
29
+ throw new Error('set() must be implemented');
30
+ }
31
+
32
+ async delete(key) {
33
+ throw new Error('delete() must be implemented');
34
+ }
35
+
36
+ async clear() {
37
+ throw new Error('clear() must be implemented');
38
+ }
39
+
40
+ async keys(pattern = null) {
41
+ throw new Error('keys() must be implemented');
42
+ }
43
+
44
+ async has(key) {
45
+ throw new Error('has() must be implemented');
46
+ }
47
+ }
48
+
49
+ /* ---------------- Memory Adapter (Built-in) ---------------- */
50
+ class MemoryAdapter extends DatabaseAdapter {
51
+ constructor(config) {
52
+ super(config);
53
+ this.store = new Map();
54
+ this.timers = new Map();
55
+ }
56
+
57
+ async connect() {
58
+ this.connected = true;
59
+ return true;
60
+ }
61
+
62
+ async disconnect() {
63
+ this.store.clear();
64
+ this.timers.forEach(timer => clearTimeout(timer));
65
+ this.timers.clear();
66
+ this.connected = false;
67
+ return true;
68
+ }
69
+
70
+ async get(key) {
71
+ return this.store.get(key) || null;
72
+ }
73
+
74
+ async set(key, value, ttl = null) {
75
+ this.store.set(key, value);
76
+
77
+ // Clear existing timer
78
+ if (this.timers.has(key)) {
79
+ clearTimeout(this.timers.get(key));
80
+ }
81
+
82
+ // Set TTL timer
83
+ if (ttl) {
84
+ const timer = setTimeout(() => {
85
+ this.store.delete(key);
86
+ this.timers.delete(key);
87
+ }, ttl);
88
+ this.timers.set(key, timer);
89
+ }
90
+
91
+ return true;
92
+ }
93
+
94
+ async delete(key) {
95
+ if (this.timers.has(key)) {
96
+ clearTimeout(this.timers.get(key));
97
+ this.timers.delete(key);
98
+ }
99
+ return this.store.delete(key);
100
+ }
101
+
102
+ async clear() {
103
+ const count = this.store.size;
104
+ this.store.clear();
105
+ this.timers.forEach(timer => clearTimeout(timer));
106
+ this.timers.clear();
107
+ return count;
108
+ }
109
+
110
+ async keys(pattern = null) {
111
+ const allKeys = Array.from(this.store.keys());
112
+
113
+ if (!pattern) return allKeys;
114
+
115
+ const regex = new RegExp(pattern.replace(/\*/g, '.*'));
116
+ return allKeys.filter(key => regex.test(key));
117
+ }
118
+
119
+ async has(key) {
120
+ return this.store.has(key);
121
+ }
122
+ }
123
+
124
+ /* ---------------- MongoDB Adapter ---------------- */
125
+ class MongoDBAdapter extends DatabaseAdapter {
126
+ constructor(config) {
127
+ super(config);
128
+ this.client = null;
129
+ this.db = null;
130
+ this.collection = null;
131
+ }
132
+
133
+ async connect() {
134
+ try {
135
+ // Dynamic import of mongodb
136
+ const { MongoClient } = await import('mongodb').catch(() => {
137
+ throw new Error(
138
+ '❌ MongoDB package not found.\n\n' +
139
+ ' Install it with: npm install mongodb\n\n' +
140
+ ' Then restart your server.'
141
+ );
142
+ });
143
+
144
+ const uri = this.config.uri || this.config.url;
145
+ if (!uri) {
146
+ throw new Error('MongoDB URI is required in config');
147
+ }
148
+
149
+ this.client = new MongoClient(uri, this.config.options || {});
150
+ await this.client.connect();
151
+
152
+ const dbName = this.config.database || 'triva';
153
+ this.db = this.client.db(dbName);
154
+ this.collection = this.db.collection(this.config.collection || 'cache');
155
+
156
+ // Create TTL index for automatic expiration
157
+ await this.collection.createIndex(
158
+ { expiresAt: 1 },
159
+ { expireAfterSeconds: 0 }
160
+ );
161
+
162
+ this.connected = true;
163
+ console.log('✅ Connected to MongoDB');
164
+ return true;
165
+ } catch (error) {
166
+ console.error('❌ MongoDB connection failed:', error.message);
167
+ throw error;
168
+ }
169
+ }
170
+
171
+ async disconnect() {
172
+ if (this.client) {
173
+ await this.client.close();
174
+ this.connected = false;
175
+ console.log('✅ Disconnected from MongoDB');
176
+ }
177
+ return true;
178
+ }
179
+
180
+ async get(key) {
181
+ const doc = await this.collection.findOne({ _id: key });
182
+ return doc ? doc.value : null;
183
+ }
184
+
185
+ async set(key, value, ttl = null) {
186
+ const doc = {
187
+ _id: key,
188
+ value,
189
+ createdAt: new Date()
190
+ };
191
+
192
+ if (ttl) {
193
+ doc.expiresAt = new Date(Date.now() + ttl);
194
+ }
195
+
196
+ await this.collection.replaceOne(
197
+ { _id: key },
198
+ doc,
199
+ { upsert: true }
200
+ );
201
+
202
+ return true;
203
+ }
204
+
205
+ async delete(key) {
206
+ const result = await this.collection.deleteOne({ _id: key });
207
+ return result.deletedCount > 0;
208
+ }
209
+
210
+ async clear() {
211
+ const result = await this.collection.deleteMany({});
212
+ return result.deletedCount;
213
+ }
214
+
215
+ async keys(pattern = null) {
216
+ const query = pattern
217
+ ? { _id: { $regex: pattern.replace(/\*/g, '.*') } }
218
+ : {};
219
+
220
+ const docs = await this.collection.find(query, { projection: { _id: 1 } }).toArray();
221
+ return docs.map(doc => doc._id);
222
+ }
223
+
224
+ async has(key) {
225
+ const count = await this.collection.countDocuments({ _id: key });
226
+ return count > 0;
227
+ }
228
+ }
229
+
230
+ /* ---------------- Redis Adapter ---------------- */
231
+ class RedisAdapter extends DatabaseAdapter {
232
+ constructor(config) {
233
+ super(config);
234
+ this.client = null;
235
+ }
236
+
237
+ async connect() {
238
+ try {
239
+ // Dynamic import of redis
240
+ const redis = await import('redis').catch(() => {
241
+ throw new Error(
242
+ '❌ Redis package not found.\n\n' +
243
+ ' Install it with: npm install redis\n\n' +
244
+ ' Then restart your server.'
245
+ );
246
+ });
247
+
248
+ this.client = redis.createClient(this.config);
249
+
250
+ this.client.on('error', (err) => {
251
+ console.error('❌ Redis error:', err);
252
+ });
253
+
254
+ await this.client.connect();
255
+ this.connected = true;
256
+ console.log('✅ Connected to Redis');
257
+ return true;
258
+ } catch (error) {
259
+ console.error('❌ Redis connection failed:', error.message);
260
+ throw error;
261
+ }
262
+ }
263
+
264
+ async disconnect() {
265
+ if (this.client) {
266
+ await this.client.quit();
267
+ this.connected = false;
268
+ console.log('✅ Disconnected from Redis');
269
+ }
270
+ return true;
271
+ }
272
+
273
+ async get(key) {
274
+ const value = await this.client.get(key);
275
+ return value ? JSON.parse(value) : null;
276
+ }
277
+
278
+ async set(key, value, ttl = null) {
279
+ const serialized = JSON.stringify(value);
280
+
281
+ if (ttl) {
282
+ await this.client.setEx(key, Math.floor(ttl / 1000), serialized);
283
+ } else {
284
+ await this.client.set(key, serialized);
285
+ }
286
+
287
+ return true;
288
+ }
289
+
290
+ async delete(key) {
291
+ const result = await this.client.del(key);
292
+ return result > 0;
293
+ }
294
+
295
+ async clear() {
296
+ await this.client.flushDb();
297
+ return 0; // Redis doesn't return count
298
+ }
299
+
300
+ async keys(pattern = null) {
301
+ const searchPattern = pattern ? pattern : '*';
302
+ return await this.client.keys(searchPattern);
303
+ }
304
+
305
+ async has(key) {
306
+ const exists = await this.client.exists(key);
307
+ return exists === 1;
308
+ }
309
+ }
310
+
311
+ /* ---------------- PostgreSQL Adapter ---------------- */
312
+ class PostgreSQLAdapter extends DatabaseAdapter {
313
+ constructor(config) {
314
+ super(config);
315
+ this.pool = null;
316
+ }
317
+
318
+ async connect() {
319
+ try {
320
+ // Dynamic import of pg
321
+ const pg = await import('pg').catch(() => {
322
+ throw new Error(
323
+ '❌ PostgreSQL package not found.\n\n' +
324
+ ' Install it with: npm install pg\n\n' +
325
+ ' Then restart your server.'
326
+ );
327
+ });
328
+
329
+ this.pool = new pg.Pool(this.config);
330
+
331
+ // Create table if not exists
332
+ const tableName = this.config.tableName || 'triva_cache';
333
+ await this.pool.query(`
334
+ CREATE TABLE IF NOT EXISTS ${tableName} (
335
+ key TEXT PRIMARY KEY,
336
+ value JSONB NOT NULL,
337
+ expires_at TIMESTAMP
338
+ )
339
+ `);
340
+
341
+ // Create index for expiration
342
+ await this.pool.query(`
343
+ CREATE INDEX IF NOT EXISTS idx_expires_at
344
+ ON ${tableName} (expires_at)
345
+ `);
346
+
347
+ this.connected = true;
348
+ console.log('✅ Connected to PostgreSQL');
349
+ return true;
350
+ } catch (error) {
351
+ console.error('❌ PostgreSQL connection failed:', error.message);
352
+ throw error;
353
+ }
354
+ }
355
+
356
+ async disconnect() {
357
+ if (this.pool) {
358
+ await this.pool.end();
359
+ this.connected = false;
360
+ console.log('✅ Disconnected from PostgreSQL');
361
+ }
362
+ return true;
363
+ }
364
+
365
+ async get(key) {
366
+ const tableName = this.config.tableName || 'triva_cache';
367
+ const result = await this.pool.query(
368
+ `SELECT value FROM ${tableName}
369
+ WHERE key = $1
370
+ AND (expires_at IS NULL OR expires_at > NOW())`,
371
+ [key]
372
+ );
373
+
374
+ return result.rows.length > 0 ? result.rows[0].value : null;
375
+ }
376
+
377
+ async set(key, value, ttl = null) {
378
+ const tableName = this.config.tableName || 'triva_cache';
379
+ const expiresAt = ttl ? new Date(Date.now() + ttl) : null;
380
+
381
+ await this.pool.query(
382
+ `INSERT INTO ${tableName} (key, value, expires_at)
383
+ VALUES ($1, $2, $3)
384
+ ON CONFLICT (key)
385
+ DO UPDATE SET value = $2, expires_at = $3`,
386
+ [key, value, expiresAt]
387
+ );
388
+
389
+ return true;
390
+ }
391
+
392
+ async delete(key) {
393
+ const tableName = this.config.tableName || 'triva_cache';
394
+ const result = await this.pool.query(
395
+ `DELETE FROM ${tableName} WHERE key = $1`,
396
+ [key]
397
+ );
398
+ return result.rowCount > 0;
399
+ }
400
+
401
+ async clear() {
402
+ const tableName = this.config.tableName || 'triva_cache';
403
+ const result = await this.pool.query(`DELETE FROM ${tableName}`);
404
+ return result.rowCount;
405
+ }
406
+
407
+ async keys(pattern = null) {
408
+ const tableName = this.config.tableName || 'triva_cache';
409
+ const query = pattern
410
+ ? `SELECT key FROM ${tableName} WHERE key ~ $1`
411
+ : `SELECT key FROM ${tableName}`;
412
+
413
+ const params = pattern ? [pattern.replace(/\*/g, '.*')] : [];
414
+ const result = await this.pool.query(query, params);
415
+
416
+ return result.rows.map(row => row.key);
417
+ }
418
+
419
+ async has(key) {
420
+ const tableName = this.config.tableName || 'triva_cache';
421
+ const result = await this.pool.query(
422
+ `SELECT 1 FROM ${tableName} WHERE key = $1 LIMIT 1`,
423
+ [key]
424
+ );
425
+ return result.rows.length > 0;
426
+ }
427
+ }
428
+
429
+ /* ---------------- MySQL Adapter ---------------- */
430
+ class MySQLAdapter extends DatabaseAdapter {
431
+ constructor(config) {
432
+ super(config);
433
+ this.pool = null;
434
+ }
435
+
436
+ async connect() {
437
+ try {
438
+ // Dynamic import of mysql2
439
+ const mysql = await import('mysql2/promise').catch(() => {
440
+ throw new Error(
441
+ '❌ MySQL package not found.\n\n' +
442
+ ' Install it with: npm install mysql2\n\n' +
443
+ ' Then restart your server.'
444
+ );
445
+ });
446
+
447
+ this.pool = mysql.createPool(this.config);
448
+
449
+ // Create table if not exists
450
+ const tableName = this.config.tableName || 'triva_cache';
451
+ await this.pool.query(`
452
+ CREATE TABLE IF NOT EXISTS ${tableName} (
453
+ \`key\` VARCHAR(255) PRIMARY KEY,
454
+ \`value\` JSON NOT NULL,
455
+ \`expires_at\` TIMESTAMP NULL,
456
+ INDEX idx_expires_at (expires_at)
457
+ )
458
+ `);
459
+
460
+ this.connected = true;
461
+ console.log('✅ Connected to MySQL');
462
+ return true;
463
+ } catch (error) {
464
+ console.error('❌ MySQL connection failed:', error.message);
465
+ throw error;
466
+ }
467
+ }
468
+
469
+ async disconnect() {
470
+ if (this.pool) {
471
+ await this.pool.end();
472
+ this.connected = false;
473
+ console.log('✅ Disconnected from MySQL');
474
+ }
475
+ return true;
476
+ }
477
+
478
+ async get(key) {
479
+ const tableName = this.config.tableName || 'triva_cache';
480
+ const [rows] = await this.pool.query(
481
+ `SELECT value FROM ${tableName}
482
+ WHERE \`key\` = ?
483
+ AND (expires_at IS NULL OR expires_at > NOW())`,
484
+ [key]
485
+ );
486
+
487
+ return rows.length > 0 ? rows[0].value : null;
488
+ }
489
+
490
+ async set(key, value, ttl = null) {
491
+ const tableName = this.config.tableName || 'triva_cache';
492
+ const expiresAt = ttl ? new Date(Date.now() + ttl) : null;
493
+
494
+ await this.pool.query(
495
+ `INSERT INTO ${tableName} (\`key\`, value, expires_at)
496
+ VALUES (?, ?, ?)
497
+ ON DUPLICATE KEY UPDATE value = ?, expires_at = ?`,
498
+ [key, value, expiresAt, value, expiresAt]
499
+ );
500
+
501
+ return true;
502
+ }
503
+
504
+ async delete(key) {
505
+ const tableName = this.config.tableName || 'triva_cache';
506
+ const [result] = await this.pool.query(
507
+ `DELETE FROM ${tableName} WHERE \`key\` = ?`,
508
+ [key]
509
+ );
510
+ return result.affectedRows > 0;
511
+ }
512
+
513
+ async clear() {
514
+ const tableName = this.config.tableName || 'triva_cache';
515
+ const [result] = await this.pool.query(`DELETE FROM ${tableName}`);
516
+ return result.affectedRows;
517
+ }
518
+
519
+ async keys(pattern = null) {
520
+ const tableName = this.config.tableName || 'triva_cache';
521
+ const query = pattern
522
+ ? `SELECT \`key\` FROM ${tableName} WHERE \`key\` REGEXP ?`
523
+ : `SELECT \`key\` FROM ${tableName}`;
524
+
525
+ const params = pattern ? [pattern.replace(/\*/g, '.*')] : [];
526
+ const [rows] = await this.pool.query(query, params);
527
+
528
+ return rows.map(row => row.key);
529
+ }
530
+
531
+ async has(key) {
532
+ const tableName = this.config.tableName || 'triva_cache';
533
+ const [rows] = await this.pool.query(
534
+ `SELECT 1 FROM ${tableName} WHERE \`key\` = ? LIMIT 1`,
535
+ [key]
536
+ );
537
+ return rows.length > 0;
538
+ }
539
+ }
540
+
541
+ /* ---------------- Adapter Factory ---------------- */
542
+ function createAdapter(type, config) {
543
+ const adapters = {
544
+ 'memory': MemoryAdapter,
545
+ 'local': MemoryAdapter,
546
+ 'mongodb': MongoDBAdapter,
547
+ 'mongo': MongoDBAdapter,
548
+ 'redis': RedisAdapter,
549
+ 'postgresql': PostgreSQLAdapter,
550
+ 'postgres': PostgreSQLAdapter,
551
+ 'pg': PostgreSQLAdapter,
552
+ 'mysql': MySQLAdapter
553
+ };
554
+
555
+ const AdapterClass = adapters[type.toLowerCase()];
556
+
557
+ if (!AdapterClass) {
558
+ throw new Error(
559
+ `❌ Unknown database type: "${type}"\n\n` +
560
+ ` Supported types:\n` +
561
+ ` - memory/local (built-in, no package needed)\n` +
562
+ ` - mongodb/mongo (requires: npm install mongodb)\n` +
563
+ ` - redis (requires: npm install redis)\n` +
564
+ ` - postgresql/postgres/pg (requires: npm install pg)\n` +
565
+ ` - mysql (requires: npm install mysql2)\n`
566
+ );
567
+ }
568
+
569
+ return new AdapterClass(config);
570
+ }
571
+
572
+ export {
573
+ DatabaseAdapter,
574
+ MemoryAdapter,
575
+ MongoDBAdapter,
576
+ RedisAdapter,
577
+ PostgreSQLAdapter,
578
+ MySQLAdapter,
579
+ createAdapter
580
+ };