@ereo/db 0.1.6

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/index.js ADDED
@@ -0,0 +1,505 @@
1
+ // @bun
2
+ // src/adapter.ts
3
+ var adapterRegistry = new Map;
4
+ function registerAdapter(name, adapter) {
5
+ adapterRegistry.set(name, adapter);
6
+ }
7
+ function getAdapter(name) {
8
+ return adapterRegistry.get(name);
9
+ }
10
+ function getDefaultAdapter() {
11
+ const first = adapterRegistry.values().next();
12
+ return first.done ? undefined : first.value;
13
+ }
14
+ function clearAdapterRegistry() {
15
+ adapterRegistry.clear();
16
+ }
17
+ // src/dedup.ts
18
+ var DEDUP_CACHE_KEY = "__ereo_db_dedup_cache";
19
+ var DEDUP_STATS_KEY = "__ereo_db_dedup_stats";
20
+ function generateFingerprint(query, params) {
21
+ const normalizedQuery = query.trim().replace(/\s+/g, " ");
22
+ const serializedParams = params ? JSON.stringify(params, (_, value) => {
23
+ if (value instanceof Date) {
24
+ return `__date:${value.toISOString()}`;
25
+ }
26
+ if (typeof value === "bigint") {
27
+ return `__bigint:${value.toString()}`;
28
+ }
29
+ if (value === undefined) {
30
+ return "__undefined";
31
+ }
32
+ return value;
33
+ }) : "";
34
+ return `${hashString(normalizedQuery)}:${hashString(serializedParams)}`;
35
+ }
36
+ function hashString(str) {
37
+ let hash = 5381;
38
+ for (let i = 0;i < str.length; i++) {
39
+ hash = hash * 33 ^ str.charCodeAt(i);
40
+ }
41
+ return (hash >>> 0).toString(36);
42
+ }
43
+ function getCache(context) {
44
+ let cache = context.get(DEDUP_CACHE_KEY);
45
+ if (!cache) {
46
+ cache = new Map;
47
+ context.set(DEDUP_CACHE_KEY, cache);
48
+ }
49
+ return cache;
50
+ }
51
+ function getStats(context) {
52
+ let stats = context.get(DEDUP_STATS_KEY);
53
+ if (!stats) {
54
+ stats = { total: 0, hits: 0, misses: 0 };
55
+ context.set(DEDUP_STATS_KEY, stats);
56
+ }
57
+ return stats;
58
+ }
59
+ async function dedupQuery(context, query, params, executor, options) {
60
+ const stats = getStats(context);
61
+ stats.total++;
62
+ if (options?.noCache) {
63
+ const result2 = await executor();
64
+ stats.misses++;
65
+ return {
66
+ result: result2,
67
+ fromCache: false,
68
+ cacheKey: ""
69
+ };
70
+ }
71
+ const cache = getCache(context);
72
+ const cacheKey = generateFingerprint(query, params);
73
+ const cached = cache.get(cacheKey);
74
+ if (cached) {
75
+ stats.hits++;
76
+ return {
77
+ result: cached.result,
78
+ fromCache: true,
79
+ cacheKey
80
+ };
81
+ }
82
+ const result = await executor();
83
+ stats.misses++;
84
+ cache.set(cacheKey, {
85
+ result,
86
+ timestamp: Date.now(),
87
+ tables: options?.tables
88
+ });
89
+ return {
90
+ result,
91
+ fromCache: false,
92
+ cacheKey
93
+ };
94
+ }
95
+ function clearDedupCache(context) {
96
+ const cache = context.get(DEDUP_CACHE_KEY);
97
+ if (cache) {
98
+ cache.clear();
99
+ }
100
+ }
101
+ function invalidateTables(context, tables) {
102
+ const cache = context.get(DEDUP_CACHE_KEY);
103
+ if (!cache)
104
+ return;
105
+ const tableSet = new Set(tables.map((t) => t.toLowerCase()));
106
+ for (const [key, entry] of cache.entries()) {
107
+ if (entry.tables) {
108
+ const hasOverlap = entry.tables.some((t) => tableSet.has(t.toLowerCase()));
109
+ if (hasOverlap) {
110
+ cache.delete(key);
111
+ }
112
+ }
113
+ }
114
+ }
115
+ function getRequestDedupStats(context) {
116
+ const stats = context.get(DEDUP_STATS_KEY);
117
+ if (!stats) {
118
+ return {
119
+ total: 0,
120
+ deduplicated: 0,
121
+ unique: 0,
122
+ hitRate: 0
123
+ };
124
+ }
125
+ return {
126
+ total: stats.total,
127
+ deduplicated: stats.hits,
128
+ unique: stats.misses,
129
+ hitRate: stats.total > 0 ? stats.hits / stats.total : 0
130
+ };
131
+ }
132
+ function debugGetCacheContents(context) {
133
+ const cache = context.get(DEDUP_CACHE_KEY);
134
+ if (!cache)
135
+ return [];
136
+ const now = Date.now();
137
+ return Array.from(cache.entries()).map(([key, entry]) => ({
138
+ key,
139
+ tables: entry.tables,
140
+ age: now - entry.timestamp
141
+ }));
142
+ }
143
+ // src/types.ts
144
+ class DatabaseError extends Error {
145
+ code;
146
+ cause;
147
+ constructor(message, code, cause) {
148
+ super(message);
149
+ this.code = code;
150
+ this.cause = cause;
151
+ this.name = "DatabaseError";
152
+ }
153
+ }
154
+
155
+ class ConnectionError extends DatabaseError {
156
+ constructor(message, cause) {
157
+ super(message, "CONNECTION_ERROR", cause);
158
+ this.name = "ConnectionError";
159
+ }
160
+ }
161
+
162
+ class QueryError extends DatabaseError {
163
+ query;
164
+ params;
165
+ constructor(message, query, params, cause) {
166
+ super(message, "QUERY_ERROR", cause);
167
+ this.query = query;
168
+ this.params = params;
169
+ this.name = "QueryError";
170
+ }
171
+ }
172
+
173
+ class TransactionError extends DatabaseError {
174
+ constructor(message, cause) {
175
+ super(message, "TRANSACTION_ERROR", cause);
176
+ this.name = "TransactionError";
177
+ }
178
+ }
179
+
180
+ class TimeoutError extends DatabaseError {
181
+ timeoutMs;
182
+ constructor(message, timeoutMs) {
183
+ super(message, "TIMEOUT_ERROR");
184
+ this.timeoutMs = timeoutMs;
185
+ this.name = "TimeoutError";
186
+ }
187
+ }
188
+
189
+ // src/pool.ts
190
+ var DEFAULT_POOL_CONFIG = {
191
+ min: 2,
192
+ max: 10,
193
+ idleTimeoutMs: 30000,
194
+ acquireTimeoutMs: 1e4,
195
+ acquireRetries: 3
196
+ };
197
+ function createEdgePoolConfig(overrides) {
198
+ return {
199
+ min: 0,
200
+ max: 1,
201
+ idleTimeoutMs: 0,
202
+ acquireTimeoutMs: 5000,
203
+ acquireRetries: 2,
204
+ ...overrides
205
+ };
206
+ }
207
+ function createServerlessPoolConfig(overrides) {
208
+ return {
209
+ min: 0,
210
+ max: 5,
211
+ idleTimeoutMs: 1e4,
212
+ acquireTimeoutMs: 8000,
213
+ acquireRetries: 2,
214
+ ...overrides
215
+ };
216
+ }
217
+
218
+ class ConnectionPool {
219
+ config;
220
+ connections = [];
221
+ activeConnections = new Set;
222
+ waitQueue = [];
223
+ closed = false;
224
+ stats = {
225
+ totalCreated: 0,
226
+ totalClosed: 0
227
+ };
228
+ constructor(config) {
229
+ this.config = {
230
+ ...DEFAULT_POOL_CONFIG,
231
+ ...config
232
+ };
233
+ }
234
+ async acquire() {
235
+ if (this.closed) {
236
+ throw new ConnectionError("Pool is closed");
237
+ }
238
+ const idle = this.connections.pop();
239
+ if (idle) {
240
+ try {
241
+ const isValid = await this.validateConnection(idle);
242
+ if (isValid) {
243
+ this.activeConnections.add(idle);
244
+ return idle;
245
+ }
246
+ await this.closeConnection(idle);
247
+ this.stats.totalClosed++;
248
+ } catch {}
249
+ }
250
+ if (this.activeConnections.size + this.connections.length < this.config.max) {
251
+ return this.createAndAcquire();
252
+ }
253
+ return this.waitForConnection();
254
+ }
255
+ async release(connection) {
256
+ if (!this.activeConnections.has(connection)) {
257
+ return;
258
+ }
259
+ this.activeConnections.delete(connection);
260
+ if (this.waitQueue.length > 0) {
261
+ const waiter = this.waitQueue.shift();
262
+ clearTimeout(waiter.timeout);
263
+ this.activeConnections.add(connection);
264
+ waiter.resolve(connection);
265
+ return;
266
+ }
267
+ if (!this.closed && this.connections.length < this.config.max) {
268
+ this.connections.push(connection);
269
+ this.scheduleIdleTimeout(connection);
270
+ } else {
271
+ await this.closeConnection(connection);
272
+ this.stats.totalClosed++;
273
+ }
274
+ }
275
+ async close() {
276
+ this.closed = true;
277
+ for (const waiter of this.waitQueue) {
278
+ clearTimeout(waiter.timeout);
279
+ waiter.reject(new ConnectionError("Pool is closing"));
280
+ }
281
+ this.waitQueue = [];
282
+ const closePromises = this.connections.map(async (conn) => {
283
+ try {
284
+ await this.closeConnection(conn);
285
+ this.stats.totalClosed++;
286
+ } catch {}
287
+ });
288
+ this.connections = [];
289
+ await Promise.all(closePromises);
290
+ }
291
+ getStats() {
292
+ return {
293
+ active: this.activeConnections.size,
294
+ idle: this.connections.length,
295
+ total: this.activeConnections.size + this.connections.length,
296
+ waiting: this.waitQueue.length,
297
+ totalCreated: this.stats.totalCreated,
298
+ totalClosed: this.stats.totalClosed
299
+ };
300
+ }
301
+ async healthCheck() {
302
+ const start = Date.now();
303
+ try {
304
+ const conn = await this.acquire();
305
+ const isValid = await this.validateConnection(conn);
306
+ await this.release(conn);
307
+ return {
308
+ healthy: isValid,
309
+ latencyMs: Date.now() - start,
310
+ metadata: this.getStats()
311
+ };
312
+ } catch (error) {
313
+ return {
314
+ healthy: false,
315
+ latencyMs: Date.now() - start,
316
+ error: error instanceof Error ? error.message : String(error),
317
+ metadata: this.getStats()
318
+ };
319
+ }
320
+ }
321
+ async createAndAcquire() {
322
+ let lastError;
323
+ for (let attempt = 0;attempt < this.config.acquireRetries; attempt++) {
324
+ try {
325
+ const connection = await this.createConnection();
326
+ this.stats.totalCreated++;
327
+ this.activeConnections.add(connection);
328
+ return connection;
329
+ } catch (error) {
330
+ lastError = error instanceof Error ? error : new Error(String(error));
331
+ if (attempt < this.config.acquireRetries - 1) {
332
+ await this.delay(Math.pow(2, attempt) * 100);
333
+ }
334
+ }
335
+ }
336
+ throw new ConnectionError(`Failed to create connection after ${this.config.acquireRetries} attempts`, lastError);
337
+ }
338
+ waitForConnection() {
339
+ return new Promise((resolve, reject) => {
340
+ const timeout = setTimeout(() => {
341
+ const index = this.waitQueue.findIndex((w) => w.resolve === resolve);
342
+ if (index !== -1) {
343
+ this.waitQueue.splice(index, 1);
344
+ }
345
+ reject(new TimeoutError("Timed out waiting for connection", this.config.acquireTimeoutMs));
346
+ }, this.config.acquireTimeoutMs);
347
+ this.waitQueue.push({ resolve, reject, timeout });
348
+ });
349
+ }
350
+ scheduleIdleTimeout(connection) {
351
+ if (this.config.idleTimeoutMs <= 0)
352
+ return;
353
+ setTimeout(async () => {
354
+ const index = this.connections.indexOf(connection);
355
+ if (index !== -1) {
356
+ this.connections.splice(index, 1);
357
+ try {
358
+ await this.closeConnection(connection);
359
+ this.stats.totalClosed++;
360
+ } catch {}
361
+ }
362
+ }, this.config.idleTimeoutMs);
363
+ }
364
+ delay(ms) {
365
+ return new Promise((resolve) => setTimeout(resolve, ms));
366
+ }
367
+ }
368
+ var DEFAULT_RETRY_CONFIG = {
369
+ maxAttempts: 3,
370
+ baseDelayMs: 100,
371
+ maxDelayMs: 5000,
372
+ exponential: true
373
+ };
374
+ async function withRetry(operation, config = {}) {
375
+ const {
376
+ maxAttempts,
377
+ baseDelayMs,
378
+ maxDelayMs,
379
+ exponential,
380
+ isRetryable
381
+ } = { ...DEFAULT_RETRY_CONFIG, ...config };
382
+ let lastError;
383
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
384
+ try {
385
+ return await operation();
386
+ } catch (error) {
387
+ lastError = error instanceof Error ? error : new Error(String(error));
388
+ if (isRetryable && !isRetryable(lastError)) {
389
+ throw lastError;
390
+ }
391
+ if (attempt < maxAttempts) {
392
+ const delay = exponential ? Math.min(baseDelayMs * Math.pow(2, attempt - 1), maxDelayMs) : baseDelayMs;
393
+ await new Promise((resolve) => setTimeout(resolve, delay));
394
+ }
395
+ }
396
+ }
397
+ throw lastError;
398
+ }
399
+ function isCommonRetryableError(error) {
400
+ const message = error.message.toLowerCase();
401
+ if (message.includes("connection") && (message.includes("refused") || message.includes("reset") || message.includes("closed") || message.includes("timeout"))) {
402
+ return true;
403
+ }
404
+ if (message.includes("deadlock")) {
405
+ return true;
406
+ }
407
+ if (message.includes("serialization failure") || message.includes("could not serialize")) {
408
+ return true;
409
+ }
410
+ if (message.includes("too many connections")) {
411
+ return true;
412
+ }
413
+ return false;
414
+ }
415
+ // src/plugin.ts
416
+ var DB_CLIENT_KEY = "__ereo_db_client";
417
+ var DB_ADAPTER_KEY = "__ereo_db_adapter";
418
+ function createDatabasePlugin(adapter, options = {}) {
419
+ const {
420
+ registerDefault = true,
421
+ registrationName = adapter.name,
422
+ debug = false
423
+ } = options;
424
+ const log = debug ? (...args) => console.log(`[db:${adapter.name}]`, ...args) : () => {};
425
+ return {
426
+ name: `@ereo/db:${adapter.name}`,
427
+ async setup() {
428
+ log("Initializing database adapter...");
429
+ if (registerDefault) {
430
+ registerAdapter(registrationName, adapter);
431
+ log(`Registered as "${registrationName}"`);
432
+ }
433
+ const health = await adapter.healthCheck();
434
+ if (health.healthy) {
435
+ log(`Connected successfully (${health.latencyMs}ms)`);
436
+ } else {
437
+ console.warn(`[db:${adapter.name}] Health check failed:`, health.error);
438
+ }
439
+ },
440
+ configureServer(server) {
441
+ server.middlewares.push(async (_request, context, next) => {
442
+ const client = adapter.getRequestClient(context);
443
+ context.set(DB_CLIENT_KEY, client);
444
+ context.set(DB_ADAPTER_KEY, adapter);
445
+ log("Attached request-scoped client to context");
446
+ return next();
447
+ });
448
+ }
449
+ };
450
+ }
451
+ function useDb(context) {
452
+ const client = context.get(DB_CLIENT_KEY);
453
+ if (!client) {
454
+ throw new Error("Database not available in context. " + "Ensure createDatabasePlugin is registered in your config.");
455
+ }
456
+ return client;
457
+ }
458
+ function useAdapter(context) {
459
+ const adapter = context.get(DB_ADAPTER_KEY);
460
+ if (!adapter) {
461
+ throw new Error("Database adapter not available in context. " + "Ensure createDatabasePlugin is registered in your config.");
462
+ }
463
+ return adapter;
464
+ }
465
+ function getDb() {
466
+ return getDefaultAdapter();
467
+ }
468
+ async function withTransaction(context, fn) {
469
+ const adapter = useAdapter(context);
470
+ const result = await adapter.transaction(fn);
471
+ const client = context.get(DB_CLIENT_KEY);
472
+ if (client) {
473
+ client.clearDedup();
474
+ }
475
+ return result;
476
+ }
477
+ export {
478
+ withTransaction,
479
+ withRetry,
480
+ useDb,
481
+ useAdapter,
482
+ registerAdapter,
483
+ isCommonRetryableError,
484
+ invalidateTables,
485
+ getRequestDedupStats,
486
+ getDefaultAdapter,
487
+ getDb,
488
+ getAdapter,
489
+ generateFingerprint,
490
+ dedupQuery,
491
+ debugGetCacheContents,
492
+ createServerlessPoolConfig,
493
+ createEdgePoolConfig,
494
+ createDatabasePlugin,
495
+ clearDedupCache,
496
+ clearAdapterRegistry,
497
+ TransactionError,
498
+ TimeoutError,
499
+ QueryError,
500
+ DatabaseError,
501
+ DEFAULT_RETRY_CONFIG,
502
+ DEFAULT_POOL_CONFIG,
503
+ ConnectionPool,
504
+ ConnectionError
505
+ };
@@ -0,0 +1,117 @@
1
+ /**
2
+ * @ereo/db - Database Plugin Factory
3
+ *
4
+ * Creates an EreoJS plugin that integrates a database adapter
5
+ * with the framework's request lifecycle.
6
+ */
7
+ import type { Plugin, AppContext } from '@ereo/core';
8
+ import type { DatabaseAdapter, RequestScopedClient } from './adapter';
9
+ /**
10
+ * Options for the database plugin.
11
+ */
12
+ export interface DatabasePluginOptions {
13
+ /**
14
+ * Register this adapter as the default.
15
+ * @default true
16
+ */
17
+ registerDefault?: boolean;
18
+ /**
19
+ * Name to register the adapter under.
20
+ * @default adapter.name
21
+ */
22
+ registrationName?: string;
23
+ /**
24
+ * Enable debug logging.
25
+ * @default false
26
+ */
27
+ debug?: boolean;
28
+ }
29
+ /**
30
+ * Create an EreoJS plugin for a database adapter.
31
+ * This handles the lifecycle integration:
32
+ * - Registers the adapter globally on setup
33
+ * - Attaches request-scoped clients to context via middleware
34
+ * - Handles cleanup on shutdown
35
+ *
36
+ * @param adapter - The database adapter instance
37
+ * @param options - Plugin configuration options
38
+ * @returns An EreoJS plugin
39
+ *
40
+ * @example
41
+ * import { createDatabasePlugin } from '@ereo/db';
42
+ * import { createDrizzleAdapter } from '@ereo/db-drizzle';
43
+ *
44
+ * const adapter = createDrizzleAdapter({
45
+ * driver: 'postgres-js',
46
+ * url: process.env.DATABASE_URL,
47
+ * schema,
48
+ * });
49
+ *
50
+ * export default defineConfig({
51
+ * plugins: [
52
+ * createDatabasePlugin(adapter),
53
+ * ],
54
+ * });
55
+ */
56
+ export declare function createDatabasePlugin<TSchema>(adapter: DatabaseAdapter<TSchema>, options?: DatabasePluginOptions): Plugin;
57
+ /**
58
+ * Get the database client from request context.
59
+ * Use this in loaders, actions, and middleware.
60
+ *
61
+ * @param context - The request context from EreoJS
62
+ * @returns The request-scoped database client with deduplication
63
+ * @throws Error if database plugin is not configured
64
+ *
65
+ * @example
66
+ * export const loader = createLoader({
67
+ * load: async ({ context }) => {
68
+ * const db = useDb(context);
69
+ * return db.client.select().from(users).where(eq(users.id, 1));
70
+ * },
71
+ * });
72
+ */
73
+ export declare function useDb<TSchema = unknown>(context: AppContext): RequestScopedClient<TSchema>;
74
+ /**
75
+ * Get the raw database adapter from context.
76
+ * Use this when you need direct adapter access (e.g., for transactions).
77
+ *
78
+ * @param context - The request context from EreoJS
79
+ * @returns The database adapter
80
+ * @throws Error if database plugin is not configured
81
+ */
82
+ export declare function useAdapter<TSchema = unknown>(context: AppContext): DatabaseAdapter<TSchema>;
83
+ /**
84
+ * Get the default registered database adapter.
85
+ * Use this outside of request context when you need database access.
86
+ *
87
+ * @returns The default database adapter or undefined
88
+ *
89
+ * @example
90
+ * // In a script or background job
91
+ * const adapter = getDb();
92
+ * if (adapter) {
93
+ * const users = await adapter.getClient().select().from(users);
94
+ * }
95
+ */
96
+ export declare function getDb<TSchema = unknown>(): DatabaseAdapter<TSchema> | undefined;
97
+ /**
98
+ * Run a function within a database transaction using request context.
99
+ * Convenience wrapper around adapter.transaction().
100
+ *
101
+ * @param context - The request context
102
+ * @param fn - Function to run within the transaction
103
+ * @returns The result of the function
104
+ *
105
+ * @example
106
+ * export const action = createAction({
107
+ * async run({ context }) {
108
+ * return withTransaction(context, async (tx) => {
109
+ * await tx.insert(users).values({ name: 'Alice' });
110
+ * await tx.insert(profiles).values({ userId: 1 });
111
+ * return { success: true };
112
+ * });
113
+ * },
114
+ * });
115
+ */
116
+ export declare function withTransaction<TSchema, TResult>(context: AppContext, fn: (tx: TSchema) => Promise<TResult>): Promise<TResult>;
117
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,KAAK,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAiBtE;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAC1C,OAAO,EAAE,eAAe,CAAC,OAAO,CAAC,EACjC,OAAO,GAAE,qBAA0B,GAClC,MAAM,CAsDR;AAMD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,KAAK,CAAC,OAAO,GAAG,OAAO,EACrC,OAAO,EAAE,UAAU,GAClB,mBAAmB,CAAC,OAAO,CAAC,CAW9B;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,OAAO,GAAG,OAAO,EAC1C,OAAO,EAAE,UAAU,GAClB,eAAe,CAAC,OAAO,CAAC,CAW1B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,KAAK,CAAC,OAAO,GAAG,OAAO,KAAK,eAAe,CAAC,OAAO,CAAC,GAAG,SAAS,CAE/E;AAMD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,OAAO,EACpD,OAAO,EAAE,UAAU,EACnB,EAAE,EAAE,CAAC,EAAE,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,GACpC,OAAO,CAAC,OAAO,CAAC,CAWlB"}