@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/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
|
+
};
|
package/dist/plugin.d.ts
ADDED
|
@@ -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"}
|