@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/README.md +1282 -0
- package/dist/adapter.d.ts +151 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/dedup.d.ts +70 -0
- package/dist/dedup.d.ts.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +505 -0
- package/dist/plugin.d.ts +117 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/pool.d.ts +139 -0
- package/dist/pool.d.ts.map +1 -0
- package/dist/types.d.ts +168 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,1282 @@
|
|
|
1
|
+
# @ereo/db
|
|
2
|
+
|
|
3
|
+
Database adapter abstractions for the EreoJS framework. This package provides ORM-agnostic database abstractions including adapter interfaces, query deduplication, connection pooling primitives, and comprehensive type utilities for end-to-end type safety.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Overview](#overview)
|
|
8
|
+
- [Installation](#installation)
|
|
9
|
+
- [Quick Start](#quick-start)
|
|
10
|
+
- [API Reference](#api-reference)
|
|
11
|
+
- [Plugin Factory](#plugin-factory)
|
|
12
|
+
- [Context Helpers](#context-helpers)
|
|
13
|
+
- [Adapter Interface](#adapter-interface)
|
|
14
|
+
- [Query Deduplication](#query-deduplication)
|
|
15
|
+
- [Connection Pool](#connection-pool)
|
|
16
|
+
- [Retry Utilities](#retry-utilities)
|
|
17
|
+
- [Error Classes](#error-classes)
|
|
18
|
+
- [Types](#types)
|
|
19
|
+
- [Configuration Options](#configuration-options)
|
|
20
|
+
- [Use Cases](#use-cases)
|
|
21
|
+
- [Integration with Other Packages](#integration-with-other-packages)
|
|
22
|
+
- [Troubleshooting](#troubleshooting)
|
|
23
|
+
- [TypeScript Types Reference](#typescript-types-reference)
|
|
24
|
+
|
|
25
|
+
## Overview
|
|
26
|
+
|
|
27
|
+
`@ereo/db` is the core database abstraction layer for EreoJS applications. It provides:
|
|
28
|
+
|
|
29
|
+
- **Adapter Interface**: A standardized interface that ORM-specific adapters (Drizzle, Prisma, Kysely) implement for consistent database access patterns.
|
|
30
|
+
- **Query Deduplication**: Request-scoped caching that automatically deduplicates identical queries within a single request, eliminating N+1 query problems.
|
|
31
|
+
- **Connection Pooling**: Abstract connection pool primitives with presets for server, edge, and serverless environments.
|
|
32
|
+
- **Type Utilities**: End-to-end type safety with inference utilities compatible with Drizzle ORM.
|
|
33
|
+
- **Plugin Integration**: Seamless integration with EreoJS's plugin system for automatic lifecycle management.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
# Using bun
|
|
39
|
+
bun add @ereo/db
|
|
40
|
+
|
|
41
|
+
# Using npm
|
|
42
|
+
npm install @ereo/db
|
|
43
|
+
|
|
44
|
+
# Using pnpm
|
|
45
|
+
pnpm add @ereo/db
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
You will also need an ORM-specific adapter package:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# For Drizzle ORM
|
|
52
|
+
bun add @ereo/db-drizzle drizzle-orm
|
|
53
|
+
|
|
54
|
+
# With a database driver (e.g., postgres)
|
|
55
|
+
bun add postgres
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
### 1. Configure the Database Plugin
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// ereo.config.ts
|
|
64
|
+
import { defineConfig } from '@ereo/core';
|
|
65
|
+
import { createDatabasePlugin } from '@ereo/db';
|
|
66
|
+
import { createDrizzleAdapter } from '@ereo/db-drizzle';
|
|
67
|
+
import * as schema from './db/schema';
|
|
68
|
+
|
|
69
|
+
const adapter = createDrizzleAdapter({
|
|
70
|
+
driver: 'postgres-js',
|
|
71
|
+
url: process.env.DATABASE_URL,
|
|
72
|
+
schema,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export default defineConfig({
|
|
76
|
+
plugins: [
|
|
77
|
+
createDatabasePlugin(adapter),
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### 2. Use in Route Loaders
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// routes/users.tsx
|
|
86
|
+
import { createLoader } from '@ereo/core';
|
|
87
|
+
import { useDb } from '@ereo/db';
|
|
88
|
+
import { users } from '../db/schema';
|
|
89
|
+
import { eq } from 'drizzle-orm';
|
|
90
|
+
|
|
91
|
+
export const loader = createLoader({
|
|
92
|
+
load: async ({ context, params }) => {
|
|
93
|
+
const db = useDb(context);
|
|
94
|
+
|
|
95
|
+
// Queries are automatically deduplicated within the same request
|
|
96
|
+
const user = await db.client
|
|
97
|
+
.select()
|
|
98
|
+
.from(users)
|
|
99
|
+
.where(eq(users.id, params.id));
|
|
100
|
+
|
|
101
|
+
return { user };
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 3. Use Transactions in Actions
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// routes/users.tsx
|
|
110
|
+
import { createAction } from '@ereo/core';
|
|
111
|
+
import { withTransaction } from '@ereo/db';
|
|
112
|
+
import { users, profiles } from '../db/schema';
|
|
113
|
+
|
|
114
|
+
export const action = createAction({
|
|
115
|
+
async run({ context, formData }) {
|
|
116
|
+
return withTransaction(context, async (tx) => {
|
|
117
|
+
const [user] = await tx.insert(users).values({
|
|
118
|
+
name: formData.get('name'),
|
|
119
|
+
email: formData.get('email'),
|
|
120
|
+
}).returning();
|
|
121
|
+
|
|
122
|
+
await tx.insert(profiles).values({
|
|
123
|
+
userId: user.id,
|
|
124
|
+
bio: formData.get('bio'),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return { success: true, userId: user.id };
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## API Reference
|
|
134
|
+
|
|
135
|
+
### Plugin Factory
|
|
136
|
+
|
|
137
|
+
#### `createDatabasePlugin(adapter, options?)`
|
|
138
|
+
|
|
139
|
+
Creates an EreoJS plugin that integrates a database adapter with the framework's request lifecycle.
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { createDatabasePlugin } from '@ereo/db';
|
|
143
|
+
|
|
144
|
+
const plugin = createDatabasePlugin(adapter, {
|
|
145
|
+
registerDefault: true, // Register as the default adapter (default: true)
|
|
146
|
+
registrationName: 'main', // Name for adapter registration (default: adapter.name)
|
|
147
|
+
debug: false, // Enable debug logging (default: false)
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Parameters:**
|
|
152
|
+
|
|
153
|
+
| Parameter | Type | Description |
|
|
154
|
+
|-----------|------|-------------|
|
|
155
|
+
| `adapter` | `DatabaseAdapter<TSchema>` | The database adapter instance |
|
|
156
|
+
| `options` | `DatabasePluginOptions` | Optional plugin configuration |
|
|
157
|
+
|
|
158
|
+
**Options:**
|
|
159
|
+
|
|
160
|
+
| Option | Type | Default | Description |
|
|
161
|
+
|--------|------|---------|-------------|
|
|
162
|
+
| `registerDefault` | `boolean` | `true` | Register this adapter as the default |
|
|
163
|
+
| `registrationName` | `string` | `adapter.name` | Name to register the adapter under |
|
|
164
|
+
| `debug` | `boolean` | `false` | Enable debug logging |
|
|
165
|
+
|
|
166
|
+
**Lifecycle:**
|
|
167
|
+
|
|
168
|
+
1. **Setup**: Registers the adapter globally and performs a health check
|
|
169
|
+
2. **Middleware**: Attaches request-scoped clients to context for each request
|
|
170
|
+
3. **Shutdown**: Handles cleanup when the application stops
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
### Context Helpers
|
|
175
|
+
|
|
176
|
+
#### `useDb(context)`
|
|
177
|
+
|
|
178
|
+
Get the database client from request context. Use this in loaders, actions, and middleware.
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import { useDb } from '@ereo/db';
|
|
182
|
+
|
|
183
|
+
export const loader = createLoader({
|
|
184
|
+
load: async ({ context }) => {
|
|
185
|
+
const db = useDb(context);
|
|
186
|
+
|
|
187
|
+
// Access the underlying ORM client
|
|
188
|
+
const users = await db.client.select().from(usersTable);
|
|
189
|
+
|
|
190
|
+
// Execute raw SQL with deduplication
|
|
191
|
+
const result = await db.query('SELECT * FROM users WHERE id = ?', [1]);
|
|
192
|
+
|
|
193
|
+
// Get deduplication statistics
|
|
194
|
+
const stats = db.getDedupStats();
|
|
195
|
+
console.log(`Cache hit rate: ${stats.hitRate * 100}%`);
|
|
196
|
+
|
|
197
|
+
return { users };
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Returns:** `RequestScopedClient<TSchema>`
|
|
203
|
+
|
|
204
|
+
**Throws:** `Error` if the database plugin is not configured
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
#### `useAdapter(context)`
|
|
209
|
+
|
|
210
|
+
Get the raw database adapter from context. Use this when you need direct adapter access.
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
import { useAdapter } from '@ereo/db';
|
|
214
|
+
|
|
215
|
+
export const action = createAction({
|
|
216
|
+
async run({ context }) {
|
|
217
|
+
const adapter = useAdapter(context);
|
|
218
|
+
|
|
219
|
+
// Manual transaction control
|
|
220
|
+
const tx = await adapter.beginTransaction();
|
|
221
|
+
try {
|
|
222
|
+
// ... perform operations
|
|
223
|
+
await tx.commit();
|
|
224
|
+
} catch (error) {
|
|
225
|
+
await tx.rollback();
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**Returns:** `DatabaseAdapter<TSchema>`
|
|
233
|
+
|
|
234
|
+
**Throws:** `Error` if the database plugin is not configured
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
#### `getDb()`
|
|
239
|
+
|
|
240
|
+
Get the default registered database adapter. Use this outside of request context (e.g., in scripts or background jobs).
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
import { getDb } from '@ereo/db';
|
|
244
|
+
|
|
245
|
+
// In a background job or script
|
|
246
|
+
async function cleanupOldRecords() {
|
|
247
|
+
const adapter = getDb();
|
|
248
|
+
if (!adapter) {
|
|
249
|
+
throw new Error('Database not configured');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
await adapter.execute(
|
|
253
|
+
'DELETE FROM sessions WHERE expires_at < NOW()'
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**Returns:** `DatabaseAdapter<TSchema> | undefined`
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
#### `withTransaction(context, fn)`
|
|
263
|
+
|
|
264
|
+
Run a function within a database transaction using request context. Automatically clears the deduplication cache after the transaction completes.
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
import { withTransaction } from '@ereo/db';
|
|
268
|
+
|
|
269
|
+
export const action = createAction({
|
|
270
|
+
async run({ context }) {
|
|
271
|
+
const result = await withTransaction(context, async (tx) => {
|
|
272
|
+
// All operations use the same transaction
|
|
273
|
+
const [order] = await tx.insert(orders).values({
|
|
274
|
+
userId: 1,
|
|
275
|
+
total: 99.99,
|
|
276
|
+
}).returning();
|
|
277
|
+
|
|
278
|
+
await tx.insert(orderItems).values([
|
|
279
|
+
{ orderId: order.id, productId: 1, quantity: 2 },
|
|
280
|
+
{ orderId: order.id, productId: 3, quantity: 1 },
|
|
281
|
+
]);
|
|
282
|
+
|
|
283
|
+
return order;
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
return { orderId: result.id };
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**Parameters:**
|
|
292
|
+
|
|
293
|
+
| Parameter | Type | Description |
|
|
294
|
+
|-----------|------|-------------|
|
|
295
|
+
| `context` | `AppContext` | The request context |
|
|
296
|
+
| `fn` | `(tx: TSchema) => Promise<TResult>` | Function to run within the transaction |
|
|
297
|
+
|
|
298
|
+
**Returns:** `Promise<TResult>`
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
### Adapter Interface
|
|
303
|
+
|
|
304
|
+
#### `DatabaseAdapter<TSchema>`
|
|
305
|
+
|
|
306
|
+
The core interface that all ORM adapters must implement.
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
interface DatabaseAdapter<TSchema = unknown> {
|
|
310
|
+
/** Human-readable adapter name */
|
|
311
|
+
readonly name: string;
|
|
312
|
+
|
|
313
|
+
/** Whether this adapter is compatible with edge runtimes */
|
|
314
|
+
readonly edgeCompatible: boolean;
|
|
315
|
+
|
|
316
|
+
/** Get the underlying database client */
|
|
317
|
+
getClient(): TSchema;
|
|
318
|
+
|
|
319
|
+
/** Get a request-scoped client with query deduplication */
|
|
320
|
+
getRequestClient(context: AppContext): RequestScopedClient<TSchema>;
|
|
321
|
+
|
|
322
|
+
/** Execute a raw SQL query */
|
|
323
|
+
query<T = unknown>(sql: string, params?: unknown[]): Promise<QueryResult<T>>;
|
|
324
|
+
|
|
325
|
+
/** Execute a raw SQL statement that modifies data */
|
|
326
|
+
execute(sql: string, params?: unknown[]): Promise<MutationResult>;
|
|
327
|
+
|
|
328
|
+
/** Run operations within a transaction */
|
|
329
|
+
transaction<T>(
|
|
330
|
+
fn: (tx: TSchema) => Promise<T>,
|
|
331
|
+
options?: TransactionOptions
|
|
332
|
+
): Promise<T>;
|
|
333
|
+
|
|
334
|
+
/** Begin a manual transaction */
|
|
335
|
+
beginTransaction(options?: TransactionOptions): Promise<Transaction<TSchema>>;
|
|
336
|
+
|
|
337
|
+
/** Check database connectivity and health */
|
|
338
|
+
healthCheck(): Promise<HealthCheckResult>;
|
|
339
|
+
|
|
340
|
+
/** Disconnect from the database */
|
|
341
|
+
disconnect(): Promise<void>;
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
#### `RequestScopedClient<TSchema>`
|
|
348
|
+
|
|
349
|
+
Request-scoped database client with automatic query deduplication.
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
interface RequestScopedClient<TSchema> {
|
|
353
|
+
/** The underlying database client (e.g., Drizzle instance) */
|
|
354
|
+
readonly client: TSchema;
|
|
355
|
+
|
|
356
|
+
/** Execute a raw SQL query with automatic deduplication */
|
|
357
|
+
query<T = unknown>(sql: string, params?: unknown[]): Promise<DedupResult<QueryResult<T>>>;
|
|
358
|
+
|
|
359
|
+
/** Get deduplication statistics for this request */
|
|
360
|
+
getDedupStats(): DedupStats;
|
|
361
|
+
|
|
362
|
+
/** Clear the deduplication cache */
|
|
363
|
+
clearDedup(): void;
|
|
364
|
+
|
|
365
|
+
/** Mark that a mutation has occurred, clearing relevant cache entries */
|
|
366
|
+
invalidate(tables?: string[]): void;
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
#### `Transaction<TSchema>`
|
|
373
|
+
|
|
374
|
+
Manual transaction handle for explicit control.
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
interface Transaction<TSchema> {
|
|
378
|
+
/** The transaction-scoped database client */
|
|
379
|
+
readonly client: TSchema;
|
|
380
|
+
|
|
381
|
+
/** Commit the transaction */
|
|
382
|
+
commit(): Promise<void>;
|
|
383
|
+
|
|
384
|
+
/** Rollback the transaction */
|
|
385
|
+
rollback(): Promise<void>;
|
|
386
|
+
|
|
387
|
+
/** Whether the transaction is still active */
|
|
388
|
+
readonly isActive: boolean;
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
#### Adapter Registry Functions
|
|
395
|
+
|
|
396
|
+
##### `registerAdapter(name, adapter)`
|
|
397
|
+
|
|
398
|
+
Register an adapter instance globally for access from anywhere in the application.
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
import { registerAdapter } from '@ereo/db';
|
|
402
|
+
|
|
403
|
+
registerAdapter('primary', primaryAdapter);
|
|
404
|
+
registerAdapter('readonly', readonlyAdapter);
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
##### `getAdapter(name)`
|
|
408
|
+
|
|
409
|
+
Get a registered adapter by name.
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
import { getAdapter } from '@ereo/db';
|
|
413
|
+
|
|
414
|
+
const primary = getAdapter('primary');
|
|
415
|
+
const readonly = getAdapter('readonly');
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
##### `getDefaultAdapter()`
|
|
419
|
+
|
|
420
|
+
Get the first registered adapter (useful when only one adapter is configured).
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
import { getDefaultAdapter } from '@ereo/db';
|
|
424
|
+
|
|
425
|
+
const adapter = getDefaultAdapter();
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
##### `clearAdapterRegistry()`
|
|
429
|
+
|
|
430
|
+
Clear all registered adapters. Useful for testing.
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
import { clearAdapterRegistry } from '@ereo/db';
|
|
434
|
+
|
|
435
|
+
afterEach(() => {
|
|
436
|
+
clearAdapterRegistry();
|
|
437
|
+
});
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
### Query Deduplication
|
|
443
|
+
|
|
444
|
+
Query deduplication automatically caches identical queries within a single request, eliminating redundant database calls.
|
|
445
|
+
|
|
446
|
+
#### `generateFingerprint(query, params?)`
|
|
447
|
+
|
|
448
|
+
Generate a deterministic cache key for a query and its parameters.
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
import { generateFingerprint } from '@ereo/db';
|
|
452
|
+
|
|
453
|
+
const key1 = generateFingerprint('SELECT * FROM users WHERE id = ?', [1]);
|
|
454
|
+
const key2 = generateFingerprint('SELECT * FROM users WHERE id = ?', [1]);
|
|
455
|
+
// key1 === key2
|
|
456
|
+
|
|
457
|
+
const key3 = generateFingerprint('SELECT * FROM users WHERE id = ?', [2]);
|
|
458
|
+
// key1 !== key3
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
**Features:**
|
|
462
|
+
- Normalizes whitespace in SQL queries
|
|
463
|
+
- Handles special types: `Date`, `bigint`, `undefined`
|
|
464
|
+
- Uses djb2 hashing algorithm for fast, consistent fingerprints
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
#### `dedupQuery(context, query, params, executor, options?)`
|
|
469
|
+
|
|
470
|
+
Execute a query with automatic deduplication. Returns cached results for identical queries within the same request.
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
import { dedupQuery } from '@ereo/db';
|
|
474
|
+
|
|
475
|
+
const result = await dedupQuery(
|
|
476
|
+
context,
|
|
477
|
+
'SELECT * FROM users WHERE id = ?',
|
|
478
|
+
[userId],
|
|
479
|
+
async () => {
|
|
480
|
+
// This only executes on cache miss
|
|
481
|
+
return db.query('SELECT * FROM users WHERE id = ?', [userId]);
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
tables: ['users'], // Tables affected (for selective invalidation)
|
|
485
|
+
noCache: false, // Skip caching for this query
|
|
486
|
+
}
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
console.log(result.fromCache); // true if served from cache
|
|
490
|
+
console.log(result.cacheKey); // The fingerprint used
|
|
491
|
+
console.log(result.result); // The query result
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
**Options:**
|
|
495
|
+
|
|
496
|
+
| Option | Type | Description |
|
|
497
|
+
|--------|------|-------------|
|
|
498
|
+
| `tables` | `string[]` | Tables affected by this query (for selective invalidation) |
|
|
499
|
+
| `noCache` | `boolean` | Skip caching for this query |
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
#### `clearDedupCache(context)`
|
|
504
|
+
|
|
505
|
+
Clear the entire deduplication cache for a request. Call this after mutations to ensure subsequent reads get fresh data.
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
import { clearDedupCache } from '@ereo/db';
|
|
509
|
+
|
|
510
|
+
// After a mutation
|
|
511
|
+
await db.execute('UPDATE users SET name = ? WHERE id = ?', ['Alice', 1]);
|
|
512
|
+
clearDedupCache(context);
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
#### `invalidateTables(context, tables)`
|
|
518
|
+
|
|
519
|
+
Selectively invalidate cache entries related to specific tables.
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
import { invalidateTables } from '@ereo/db';
|
|
523
|
+
|
|
524
|
+
// Only invalidate user-related cached queries
|
|
525
|
+
await db.execute('UPDATE users SET name = ? WHERE id = ?', ['Alice', 1]);
|
|
526
|
+
invalidateTables(context, ['users']);
|
|
527
|
+
|
|
528
|
+
// Queries for 'posts' table are still cached
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
**Note:** Table name matching is case-insensitive.
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
#### `getRequestDedupStats(context)`
|
|
536
|
+
|
|
537
|
+
Get deduplication statistics for the current request.
|
|
538
|
+
|
|
539
|
+
```typescript
|
|
540
|
+
import { getRequestDedupStats } from '@ereo/db';
|
|
541
|
+
|
|
542
|
+
const stats = getRequestDedupStats(context);
|
|
543
|
+
console.log({
|
|
544
|
+
total: stats.total, // Total queries attempted
|
|
545
|
+
deduplicated: stats.deduplicated, // Queries served from cache
|
|
546
|
+
unique: stats.unique, // Unique queries executed
|
|
547
|
+
hitRate: stats.hitRate, // Cache hit rate (0-1)
|
|
548
|
+
});
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
---
|
|
552
|
+
|
|
553
|
+
#### `debugGetCacheContents(context)`
|
|
554
|
+
|
|
555
|
+
Get the current cache contents for debugging. Only use in development.
|
|
556
|
+
|
|
557
|
+
```typescript
|
|
558
|
+
import { debugGetCacheContents } from '@ereo/db';
|
|
559
|
+
|
|
560
|
+
if (process.env.NODE_ENV === 'development') {
|
|
561
|
+
const contents = debugGetCacheContents(context);
|
|
562
|
+
contents.forEach(entry => {
|
|
563
|
+
console.log({
|
|
564
|
+
key: entry.key,
|
|
565
|
+
tables: entry.tables,
|
|
566
|
+
age: entry.age, // milliseconds since cached
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
### Connection Pool
|
|
575
|
+
|
|
576
|
+
Abstract connection pooling utilities that adapters can extend.
|
|
577
|
+
|
|
578
|
+
#### `ConnectionPool<T>`
|
|
579
|
+
|
|
580
|
+
Abstract base class for connection pools. Extend this to create ORM-specific pools.
|
|
581
|
+
|
|
582
|
+
```typescript
|
|
583
|
+
import { ConnectionPool } from '@ereo/db';
|
|
584
|
+
|
|
585
|
+
class PostgresPool extends ConnectionPool<pg.Client> {
|
|
586
|
+
protected async createConnection(): Promise<pg.Client> {
|
|
587
|
+
const client = new pg.Client(this.connectionString);
|
|
588
|
+
await client.connect();
|
|
589
|
+
return client;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
protected async closeConnection(connection: pg.Client): Promise<void> {
|
|
593
|
+
await connection.end();
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
protected async validateConnection(connection: pg.Client): Promise<boolean> {
|
|
597
|
+
try {
|
|
598
|
+
await connection.query('SELECT 1');
|
|
599
|
+
return true;
|
|
600
|
+
} catch {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
**Methods:**
|
|
608
|
+
|
|
609
|
+
| Method | Description |
|
|
610
|
+
|--------|-------------|
|
|
611
|
+
| `acquire()` | Acquire a connection from the pool |
|
|
612
|
+
| `release(connection)` | Release a connection back to the pool |
|
|
613
|
+
| `close()` | Close the pool and all connections |
|
|
614
|
+
| `getStats()` | Get pool statistics |
|
|
615
|
+
| `healthCheck()` | Check if the pool is healthy |
|
|
616
|
+
|
|
617
|
+
---
|
|
618
|
+
|
|
619
|
+
#### `DEFAULT_POOL_CONFIG`
|
|
620
|
+
|
|
621
|
+
Default pool configuration for server environments.
|
|
622
|
+
|
|
623
|
+
```typescript
|
|
624
|
+
import { DEFAULT_POOL_CONFIG } from '@ereo/db';
|
|
625
|
+
|
|
626
|
+
// {
|
|
627
|
+
// min: 2,
|
|
628
|
+
// max: 10,
|
|
629
|
+
// idleTimeoutMs: 30000,
|
|
630
|
+
// acquireTimeoutMs: 10000,
|
|
631
|
+
// acquireRetries: 3,
|
|
632
|
+
// }
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
---
|
|
636
|
+
|
|
637
|
+
#### `createEdgePoolConfig(overrides?)`
|
|
638
|
+
|
|
639
|
+
Create pool configuration optimized for edge environments (Cloudflare Workers, Vercel Edge).
|
|
640
|
+
|
|
641
|
+
```typescript
|
|
642
|
+
import { createEdgePoolConfig } from '@ereo/db';
|
|
643
|
+
|
|
644
|
+
const config = createEdgePoolConfig({
|
|
645
|
+
max: 2, // Override specific settings
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// {
|
|
649
|
+
// min: 0,
|
|
650
|
+
// max: 2,
|
|
651
|
+
// idleTimeoutMs: 0,
|
|
652
|
+
// acquireTimeoutMs: 5000,
|
|
653
|
+
// acquireRetries: 2,
|
|
654
|
+
// }
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
---
|
|
658
|
+
|
|
659
|
+
#### `createServerlessPoolConfig(overrides?)`
|
|
660
|
+
|
|
661
|
+
Create pool configuration for serverless environments (AWS Lambda, Vercel Functions).
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
import { createServerlessPoolConfig } from '@ereo/db';
|
|
665
|
+
|
|
666
|
+
const config = createServerlessPoolConfig();
|
|
667
|
+
|
|
668
|
+
// {
|
|
669
|
+
// min: 0,
|
|
670
|
+
// max: 5,
|
|
671
|
+
// idleTimeoutMs: 10000,
|
|
672
|
+
// acquireTimeoutMs: 8000,
|
|
673
|
+
// acquireRetries: 2,
|
|
674
|
+
// }
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
---
|
|
678
|
+
|
|
679
|
+
#### `PoolStats`
|
|
680
|
+
|
|
681
|
+
Statistics about connection pool state.
|
|
682
|
+
|
|
683
|
+
```typescript
|
|
684
|
+
interface PoolStats {
|
|
685
|
+
active: number; // Connections currently in use
|
|
686
|
+
idle: number; // Idle connections available
|
|
687
|
+
total: number; // Total connections (active + idle)
|
|
688
|
+
waiting: number; // Requests waiting for a connection
|
|
689
|
+
totalCreated: number; // Total connections created over lifetime
|
|
690
|
+
totalClosed: number; // Total connections closed over lifetime
|
|
691
|
+
}
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
---
|
|
695
|
+
|
|
696
|
+
### Retry Utilities
|
|
697
|
+
|
|
698
|
+
#### `withRetry(operation, config?)`
|
|
699
|
+
|
|
700
|
+
Execute an operation with automatic retry logic and exponential backoff.
|
|
701
|
+
|
|
702
|
+
```typescript
|
|
703
|
+
import { withRetry, isCommonRetryableError } from '@ereo/db';
|
|
704
|
+
|
|
705
|
+
const result = await withRetry(
|
|
706
|
+
async () => {
|
|
707
|
+
return db.query('SELECT * FROM users');
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
maxAttempts: 3,
|
|
711
|
+
baseDelayMs: 100,
|
|
712
|
+
maxDelayMs: 5000,
|
|
713
|
+
exponential: true,
|
|
714
|
+
isRetryable: isCommonRetryableError,
|
|
715
|
+
}
|
|
716
|
+
);
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
**Configuration:**
|
|
720
|
+
|
|
721
|
+
| Option | Type | Default | Description |
|
|
722
|
+
|--------|------|---------|-------------|
|
|
723
|
+
| `maxAttempts` | `number` | `3` | Maximum retry attempts |
|
|
724
|
+
| `baseDelayMs` | `number` | `100` | Base delay between retries |
|
|
725
|
+
| `maxDelayMs` | `number` | `5000` | Maximum delay between retries |
|
|
726
|
+
| `exponential` | `boolean` | `true` | Use exponential backoff |
|
|
727
|
+
| `isRetryable` | `(error: Error) => boolean` | - | Custom retry condition |
|
|
728
|
+
|
|
729
|
+
---
|
|
730
|
+
|
|
731
|
+
#### `isCommonRetryableError(error)`
|
|
732
|
+
|
|
733
|
+
Check if an error is commonly retryable (connection issues, deadlocks, etc.).
|
|
734
|
+
|
|
735
|
+
```typescript
|
|
736
|
+
import { isCommonRetryableError } from '@ereo/db';
|
|
737
|
+
|
|
738
|
+
// Returns true for:
|
|
739
|
+
// - Connection refused/reset/closed/timeout
|
|
740
|
+
// - Deadlock detected
|
|
741
|
+
// - Serialization failure
|
|
742
|
+
// - Too many connections
|
|
743
|
+
|
|
744
|
+
isCommonRetryableError(new Error('Connection refused')); // true
|
|
745
|
+
isCommonRetryableError(new Error('Deadlock detected')); // true
|
|
746
|
+
isCommonRetryableError(new Error('Syntax error')); // false
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
---
|
|
750
|
+
|
|
751
|
+
#### `DEFAULT_RETRY_CONFIG`
|
|
752
|
+
|
|
753
|
+
Default retry configuration.
|
|
754
|
+
|
|
755
|
+
```typescript
|
|
756
|
+
import { DEFAULT_RETRY_CONFIG } from '@ereo/db';
|
|
757
|
+
|
|
758
|
+
// {
|
|
759
|
+
// maxAttempts: 3,
|
|
760
|
+
// baseDelayMs: 100,
|
|
761
|
+
// maxDelayMs: 5000,
|
|
762
|
+
// exponential: true,
|
|
763
|
+
// }
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
---
|
|
767
|
+
|
|
768
|
+
### Error Classes
|
|
769
|
+
|
|
770
|
+
All error classes extend `DatabaseError` and include error codes for programmatic handling.
|
|
771
|
+
|
|
772
|
+
#### `DatabaseError`
|
|
773
|
+
|
|
774
|
+
Base database error class.
|
|
775
|
+
|
|
776
|
+
```typescript
|
|
777
|
+
import { DatabaseError } from '@ereo/db';
|
|
778
|
+
|
|
779
|
+
const error = new DatabaseError('Something went wrong', 'CUSTOM_CODE', cause);
|
|
780
|
+
console.log(error.name); // 'DatabaseError'
|
|
781
|
+
console.log(error.code); // 'CUSTOM_CODE'
|
|
782
|
+
console.log(error.cause); // Original error
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
---
|
|
786
|
+
|
|
787
|
+
#### `ConnectionError`
|
|
788
|
+
|
|
789
|
+
Connection-related errors (code: `CONNECTION_ERROR`).
|
|
790
|
+
|
|
791
|
+
```typescript
|
|
792
|
+
import { ConnectionError } from '@ereo/db';
|
|
793
|
+
|
|
794
|
+
try {
|
|
795
|
+
await adapter.connect();
|
|
796
|
+
} catch (error) {
|
|
797
|
+
if (error instanceof ConnectionError) {
|
|
798
|
+
console.log('Failed to connect:', error.message);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
---
|
|
804
|
+
|
|
805
|
+
#### `QueryError`
|
|
806
|
+
|
|
807
|
+
Query execution errors (code: `QUERY_ERROR`).
|
|
808
|
+
|
|
809
|
+
```typescript
|
|
810
|
+
import { QueryError } from '@ereo/db';
|
|
811
|
+
|
|
812
|
+
const error = new QueryError(
|
|
813
|
+
'Syntax error near "FORM"',
|
|
814
|
+
'SELECT * FORM users', // The problematic query
|
|
815
|
+
[1, 2, 3], // Parameters
|
|
816
|
+
originalError
|
|
817
|
+
);
|
|
818
|
+
|
|
819
|
+
console.log(error.query); // 'SELECT * FORM users'
|
|
820
|
+
console.log(error.params); // [1, 2, 3]
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
---
|
|
824
|
+
|
|
825
|
+
#### `TransactionError`
|
|
826
|
+
|
|
827
|
+
Transaction errors (code: `TRANSACTION_ERROR`).
|
|
828
|
+
|
|
829
|
+
```typescript
|
|
830
|
+
import { TransactionError } from '@ereo/db';
|
|
831
|
+
|
|
832
|
+
try {
|
|
833
|
+
await adapter.transaction(async (tx) => {
|
|
834
|
+
// ...
|
|
835
|
+
});
|
|
836
|
+
} catch (error) {
|
|
837
|
+
if (error instanceof TransactionError) {
|
|
838
|
+
console.log('Transaction failed:', error.message);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
---
|
|
844
|
+
|
|
845
|
+
#### `TimeoutError`
|
|
846
|
+
|
|
847
|
+
Timeout errors (code: `TIMEOUT_ERROR`).
|
|
848
|
+
|
|
849
|
+
```typescript
|
|
850
|
+
import { TimeoutError } from '@ereo/db';
|
|
851
|
+
|
|
852
|
+
const error = new TimeoutError('Query timed out', 5000);
|
|
853
|
+
console.log(error.timeoutMs); // 5000
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
---
|
|
857
|
+
|
|
858
|
+
### Types
|
|
859
|
+
|
|
860
|
+
#### Query Result Types
|
|
861
|
+
|
|
862
|
+
```typescript
|
|
863
|
+
/** Result of a SELECT query */
|
|
864
|
+
interface QueryResult<T = unknown> {
|
|
865
|
+
rows: T[];
|
|
866
|
+
rowCount: number;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/** Result of an INSERT/UPDATE/DELETE mutation */
|
|
870
|
+
interface MutationResult {
|
|
871
|
+
rowsAffected: number;
|
|
872
|
+
lastInsertId?: number | bigint;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/** Result wrapper with deduplication metadata */
|
|
876
|
+
interface DedupResult<T> {
|
|
877
|
+
result: T;
|
|
878
|
+
fromCache: boolean;
|
|
879
|
+
cacheKey: string;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/** Deduplication statistics */
|
|
883
|
+
interface DedupStats {
|
|
884
|
+
total: number; // Total queries attempted
|
|
885
|
+
deduplicated: number; // Queries served from cache
|
|
886
|
+
unique: number; // Unique queries executed
|
|
887
|
+
hitRate: number; // Cache hit rate (0-1)
|
|
888
|
+
}
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
---
|
|
892
|
+
|
|
893
|
+
#### Configuration Types
|
|
894
|
+
|
|
895
|
+
```typescript
|
|
896
|
+
/** Connection pool configuration */
|
|
897
|
+
interface PoolConfig {
|
|
898
|
+
min?: number; // Minimum connections (default: 2)
|
|
899
|
+
max?: number; // Maximum connections (default: 10)
|
|
900
|
+
idleTimeoutMs?: number; // Idle timeout in ms (default: 30000)
|
|
901
|
+
acquireTimeoutMs?: number; // Acquire timeout in ms (default: 10000)
|
|
902
|
+
acquireRetries?: number; // Max acquire retries (default: 3)
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/** Transaction isolation levels */
|
|
906
|
+
type IsolationLevel =
|
|
907
|
+
| 'read uncommitted'
|
|
908
|
+
| 'read committed'
|
|
909
|
+
| 'repeatable read'
|
|
910
|
+
| 'serializable';
|
|
911
|
+
|
|
912
|
+
/** Transaction configuration */
|
|
913
|
+
interface TransactionOptions {
|
|
914
|
+
isolationLevel?: IsolationLevel;
|
|
915
|
+
readOnly?: boolean;
|
|
916
|
+
timeout?: number; // Timeout in milliseconds
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/** Base adapter configuration */
|
|
920
|
+
interface AdapterConfig {
|
|
921
|
+
url: string;
|
|
922
|
+
debug?: boolean;
|
|
923
|
+
pool?: PoolConfig;
|
|
924
|
+
edgeCompatible?: boolean;
|
|
925
|
+
}
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
---
|
|
929
|
+
|
|
930
|
+
#### Type Inference Utilities
|
|
931
|
+
|
|
932
|
+
```typescript
|
|
933
|
+
/** Infer the select type from a Drizzle table */
|
|
934
|
+
type InferSelect<T extends { $inferSelect: unknown }> = T['$inferSelect'];
|
|
935
|
+
|
|
936
|
+
/** Infer the insert type from a Drizzle table */
|
|
937
|
+
type InferInsert<T extends { $inferInsert: unknown }> = T['$inferInsert'];
|
|
938
|
+
|
|
939
|
+
// Usage:
|
|
940
|
+
type User = InferSelect<typeof users>;
|
|
941
|
+
type NewUser = InferInsert<typeof users>;
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
---
|
|
945
|
+
|
|
946
|
+
#### Module Augmentation for Typed Tables
|
|
947
|
+
|
|
948
|
+
```typescript
|
|
949
|
+
// In your project's types file
|
|
950
|
+
declare module '@ereo/db' {
|
|
951
|
+
interface DatabaseTables {
|
|
952
|
+
users: typeof import('./schema').users;
|
|
953
|
+
posts: typeof import('./schema').posts;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Now you get typed table access
|
|
958
|
+
type UserTable = TableType<'users'>;
|
|
959
|
+
type AllTableNames = TableNames; // 'users' | 'posts'
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
---
|
|
963
|
+
|
|
964
|
+
#### Query Builder Types
|
|
965
|
+
|
|
966
|
+
```typescript
|
|
967
|
+
/** Typed WHERE clause conditions */
|
|
968
|
+
type TypedWhere<T> = {
|
|
969
|
+
[K in keyof T]?: T[K] | WhereOperator<T[K]>;
|
|
970
|
+
} & {
|
|
971
|
+
AND?: TypedWhere<T>[];
|
|
972
|
+
OR?: TypedWhere<T>[];
|
|
973
|
+
NOT?: TypedWhere<T>;
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
/** WHERE clause operators */
|
|
977
|
+
interface WhereOperator<T> {
|
|
978
|
+
eq?: T;
|
|
979
|
+
ne?: T;
|
|
980
|
+
gt?: T;
|
|
981
|
+
gte?: T;
|
|
982
|
+
lt?: T;
|
|
983
|
+
lte?: T;
|
|
984
|
+
in?: T[];
|
|
985
|
+
notIn?: T[];
|
|
986
|
+
like?: string;
|
|
987
|
+
ilike?: string;
|
|
988
|
+
isNull?: boolean;
|
|
989
|
+
isNotNull?: boolean;
|
|
990
|
+
between?: [T, T];
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/** Typed ORDER BY clause */
|
|
994
|
+
type TypedOrderBy<T> = {
|
|
995
|
+
[K in keyof T]?: 'asc' | 'desc';
|
|
996
|
+
};
|
|
997
|
+
|
|
998
|
+
/** Typed SELECT fields */
|
|
999
|
+
type TypedSelect<T> = (keyof T)[] | '*';
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
---
|
|
1003
|
+
|
|
1004
|
+
#### Health Check Result
|
|
1005
|
+
|
|
1006
|
+
```typescript
|
|
1007
|
+
interface HealthCheckResult {
|
|
1008
|
+
healthy: boolean;
|
|
1009
|
+
latencyMs: number;
|
|
1010
|
+
error?: string;
|
|
1011
|
+
metadata?: Record<string, unknown>;
|
|
1012
|
+
}
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
---
|
|
1016
|
+
|
|
1017
|
+
## Configuration Options
|
|
1018
|
+
|
|
1019
|
+
### Pool Configuration Presets
|
|
1020
|
+
|
|
1021
|
+
| Environment | `min` | `max` | `idleTimeoutMs` | `acquireTimeoutMs` | `acquireRetries` |
|
|
1022
|
+
|-------------|-------|-------|-----------------|--------------------|--------------------|
|
|
1023
|
+
| Server (default) | 2 | 10 | 30000 | 10000 | 3 |
|
|
1024
|
+
| Edge | 0 | 1 | 0 | 5000 | 2 |
|
|
1025
|
+
| Serverless | 0 | 5 | 10000 | 8000 | 2 |
|
|
1026
|
+
|
|
1027
|
+
---
|
|
1028
|
+
|
|
1029
|
+
## Use Cases
|
|
1030
|
+
|
|
1031
|
+
### Avoiding N+1 Queries
|
|
1032
|
+
|
|
1033
|
+
Query deduplication automatically prevents N+1 queries in nested loaders:
|
|
1034
|
+
|
|
1035
|
+
```typescript
|
|
1036
|
+
// Parent loader
|
|
1037
|
+
export const loader = createLoader({
|
|
1038
|
+
load: async ({ context }) => {
|
|
1039
|
+
const db = useDb(context);
|
|
1040
|
+
const posts = await db.client.select().from(postsTable);
|
|
1041
|
+
return { posts };
|
|
1042
|
+
},
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
// Child component that runs for each post
|
|
1046
|
+
function PostAuthor({ postId }) {
|
|
1047
|
+
// Even if this runs 100 times, the query only executes once
|
|
1048
|
+
const author = useLoaderData(async (context) => {
|
|
1049
|
+
const db = useDb(context);
|
|
1050
|
+
return db.client
|
|
1051
|
+
.select()
|
|
1052
|
+
.from(usersTable)
|
|
1053
|
+
.where(eq(usersTable.id, postId));
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
### Optimistic Updates with Cache Invalidation
|
|
1059
|
+
|
|
1060
|
+
```typescript
|
|
1061
|
+
export const action = createAction({
|
|
1062
|
+
async run({ context, formData }) {
|
|
1063
|
+
const db = useDb(context);
|
|
1064
|
+
|
|
1065
|
+
// Perform the update
|
|
1066
|
+
await db.client
|
|
1067
|
+
.update(usersTable)
|
|
1068
|
+
.set({ name: formData.get('name') })
|
|
1069
|
+
.where(eq(usersTable.id, formData.get('id')));
|
|
1070
|
+
|
|
1071
|
+
// Invalidate only user-related cached queries
|
|
1072
|
+
db.invalidate(['users']);
|
|
1073
|
+
|
|
1074
|
+
return { success: true };
|
|
1075
|
+
},
|
|
1076
|
+
});
|
|
1077
|
+
```
|
|
1078
|
+
|
|
1079
|
+
### Background Jobs with Direct Adapter Access
|
|
1080
|
+
|
|
1081
|
+
```typescript
|
|
1082
|
+
import { getDb } from '@ereo/db';
|
|
1083
|
+
|
|
1084
|
+
async function processExpiredSessions() {
|
|
1085
|
+
const adapter = getDb();
|
|
1086
|
+
if (!adapter) {
|
|
1087
|
+
throw new Error('Database not configured');
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
const result = await adapter.execute(
|
|
1091
|
+
'DELETE FROM sessions WHERE expires_at < NOW()'
|
|
1092
|
+
);
|
|
1093
|
+
|
|
1094
|
+
console.log(`Cleaned up ${result.rowsAffected} expired sessions`);
|
|
1095
|
+
}
|
|
1096
|
+
```
|
|
1097
|
+
|
|
1098
|
+
### Custom Connection Pool
|
|
1099
|
+
|
|
1100
|
+
```typescript
|
|
1101
|
+
import { ConnectionPool, createServerlessPoolConfig } from '@ereo/db';
|
|
1102
|
+
|
|
1103
|
+
class CustomPool extends ConnectionPool<MyConnection> {
|
|
1104
|
+
constructor() {
|
|
1105
|
+
super(createServerlessPoolConfig({
|
|
1106
|
+
max: 3, // Limit for serverless
|
|
1107
|
+
}));
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
protected async createConnection() {
|
|
1111
|
+
return new MyConnection(process.env.DATABASE_URL);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
protected async closeConnection(conn: MyConnection) {
|
|
1115
|
+
await conn.close();
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
protected async validateConnection(conn: MyConnection) {
|
|
1119
|
+
return conn.isAlive();
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
---
|
|
1125
|
+
|
|
1126
|
+
## Integration with Other Packages
|
|
1127
|
+
|
|
1128
|
+
### @ereo/db-drizzle
|
|
1129
|
+
|
|
1130
|
+
The official Drizzle ORM adapter for `@ereo/db`.
|
|
1131
|
+
|
|
1132
|
+
```typescript
|
|
1133
|
+
import { createDrizzleAdapter, definePostgresConfig } from '@ereo/db-drizzle';
|
|
1134
|
+
import { createDatabasePlugin } from '@ereo/db';
|
|
1135
|
+
|
|
1136
|
+
const adapter = createDrizzleAdapter(definePostgresConfig({
|
|
1137
|
+
url: process.env.DATABASE_URL,
|
|
1138
|
+
schema,
|
|
1139
|
+
}));
|
|
1140
|
+
|
|
1141
|
+
export default defineConfig({
|
|
1142
|
+
plugins: [createDatabasePlugin(adapter)],
|
|
1143
|
+
});
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
### @ereo/core
|
|
1147
|
+
|
|
1148
|
+
The database plugin integrates with EreoJS's plugin system:
|
|
1149
|
+
|
|
1150
|
+
```typescript
|
|
1151
|
+
import { defineConfig } from '@ereo/core';
|
|
1152
|
+
import { createDatabasePlugin } from '@ereo/db';
|
|
1153
|
+
|
|
1154
|
+
export default defineConfig({
|
|
1155
|
+
plugins: [
|
|
1156
|
+
createDatabasePlugin(adapter),
|
|
1157
|
+
// Other plugins...
|
|
1158
|
+
],
|
|
1159
|
+
});
|
|
1160
|
+
```
|
|
1161
|
+
|
|
1162
|
+
The plugin automatically:
|
|
1163
|
+
- Registers the adapter during setup
|
|
1164
|
+
- Attaches request-scoped clients via middleware
|
|
1165
|
+
- Performs health checks on startup
|
|
1166
|
+
|
|
1167
|
+
---
|
|
1168
|
+
|
|
1169
|
+
## Troubleshooting
|
|
1170
|
+
|
|
1171
|
+
### "Database not available in context"
|
|
1172
|
+
|
|
1173
|
+
**Cause:** `useDb()` was called before the database plugin middleware ran.
|
|
1174
|
+
|
|
1175
|
+
**Solution:** Ensure `createDatabasePlugin` is registered in your config:
|
|
1176
|
+
|
|
1177
|
+
```typescript
|
|
1178
|
+
export default defineConfig({
|
|
1179
|
+
plugins: [createDatabasePlugin(adapter)],
|
|
1180
|
+
});
|
|
1181
|
+
```
|
|
1182
|
+
|
|
1183
|
+
### "Database not connected"
|
|
1184
|
+
|
|
1185
|
+
**Cause:** Attempting to use `getClient()` before the adapter connected.
|
|
1186
|
+
|
|
1187
|
+
**Solution:** Use `useDb()` in request context, or call `healthCheck()` first:
|
|
1188
|
+
|
|
1189
|
+
```typescript
|
|
1190
|
+
const adapter = getDb();
|
|
1191
|
+
const health = await adapter.healthCheck();
|
|
1192
|
+
if (health.healthy) {
|
|
1193
|
+
const client = adapter.getClient();
|
|
1194
|
+
}
|
|
1195
|
+
```
|
|
1196
|
+
|
|
1197
|
+
### High Memory Usage from Deduplication
|
|
1198
|
+
|
|
1199
|
+
**Cause:** Very large result sets being cached.
|
|
1200
|
+
|
|
1201
|
+
**Solution:** Skip caching for large queries:
|
|
1202
|
+
|
|
1203
|
+
```typescript
|
|
1204
|
+
await dedupQuery(context, sql, params, executor, { noCache: true });
|
|
1205
|
+
```
|
|
1206
|
+
|
|
1207
|
+
### Connection Pool Exhaustion
|
|
1208
|
+
|
|
1209
|
+
**Symptoms:** `TimeoutError: Timed out waiting for connection`
|
|
1210
|
+
|
|
1211
|
+
**Solutions:**
|
|
1212
|
+
1. Increase `max` connections
|
|
1213
|
+
2. Ensure connections are properly released
|
|
1214
|
+
3. Check for connection leaks in your code
|
|
1215
|
+
4. Use appropriate pool presets for your environment
|
|
1216
|
+
|
|
1217
|
+
```typescript
|
|
1218
|
+
const config = createServerlessPoolConfig({
|
|
1219
|
+
max: 10,
|
|
1220
|
+
acquireTimeoutMs: 15000,
|
|
1221
|
+
});
|
|
1222
|
+
```
|
|
1223
|
+
|
|
1224
|
+
---
|
|
1225
|
+
|
|
1226
|
+
## TypeScript Types Reference
|
|
1227
|
+
|
|
1228
|
+
All types are exported from the main package entry point:
|
|
1229
|
+
|
|
1230
|
+
```typescript
|
|
1231
|
+
import {
|
|
1232
|
+
// Adapter types
|
|
1233
|
+
type DatabaseAdapter,
|
|
1234
|
+
type RequestScopedClient,
|
|
1235
|
+
type Transaction,
|
|
1236
|
+
type AdapterFactory,
|
|
1237
|
+
|
|
1238
|
+
// Result types
|
|
1239
|
+
type QueryResult,
|
|
1240
|
+
type MutationResult,
|
|
1241
|
+
type DedupResult,
|
|
1242
|
+
type DedupStats,
|
|
1243
|
+
|
|
1244
|
+
// Configuration types
|
|
1245
|
+
type PoolConfig,
|
|
1246
|
+
type AdapterConfig,
|
|
1247
|
+
type TransactionOptions,
|
|
1248
|
+
type IsolationLevel,
|
|
1249
|
+
type PoolStats,
|
|
1250
|
+
type RetryConfig,
|
|
1251
|
+
type HealthCheckResult,
|
|
1252
|
+
|
|
1253
|
+
// Type inference utilities
|
|
1254
|
+
type InferSelect,
|
|
1255
|
+
type InferInsert,
|
|
1256
|
+
type DatabaseTables,
|
|
1257
|
+
type TableNames,
|
|
1258
|
+
type TableType,
|
|
1259
|
+
|
|
1260
|
+
// Query builder types
|
|
1261
|
+
type TypedWhere,
|
|
1262
|
+
type WhereOperator,
|
|
1263
|
+
type TypedOrderBy,
|
|
1264
|
+
type TypedSelect,
|
|
1265
|
+
|
|
1266
|
+
// Plugin types
|
|
1267
|
+
type DatabasePluginOptions,
|
|
1268
|
+
|
|
1269
|
+
// Error classes
|
|
1270
|
+
DatabaseError,
|
|
1271
|
+
ConnectionError,
|
|
1272
|
+
QueryError,
|
|
1273
|
+
TransactionError,
|
|
1274
|
+
TimeoutError,
|
|
1275
|
+
} from '@ereo/db';
|
|
1276
|
+
```
|
|
1277
|
+
|
|
1278
|
+
---
|
|
1279
|
+
|
|
1280
|
+
## License
|
|
1281
|
+
|
|
1282
|
+
MIT
|