@wgtechlabs/nuvex 0.1.1
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/LICENSE +21 -0
- package/README.md +427 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/cjs/core/client.js +981 -0
- package/dist/cjs/core/client.js.map +1 -0
- package/dist/cjs/core/database.js +297 -0
- package/dist/cjs/core/database.js.map +1 -0
- package/dist/cjs/core/engine.js +1202 -0
- package/dist/cjs/core/engine.js.map +1 -0
- package/dist/cjs/core/index.js +35 -0
- package/dist/cjs/core/index.js.map +1 -0
- package/dist/cjs/index.js +109 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/interfaces/index.js +12 -0
- package/dist/cjs/interfaces/index.js.map +1 -0
- package/dist/cjs/layers/index.js +22 -0
- package/dist/cjs/layers/index.js.map +1 -0
- package/dist/cjs/layers/memory.js +388 -0
- package/dist/cjs/layers/memory.js.map +1 -0
- package/dist/cjs/layers/postgres.js +492 -0
- package/dist/cjs/layers/postgres.js.map +1 -0
- package/dist/cjs/layers/redis.js +388 -0
- package/dist/cjs/layers/redis.js.map +1 -0
- package/dist/cjs/types/index.js +52 -0
- package/dist/cjs/types/index.js.map +1 -0
- package/dist/esm/core/client.js +944 -0
- package/dist/esm/core/client.js.map +1 -0
- package/dist/esm/core/database.js +289 -0
- package/dist/esm/core/database.js.map +1 -0
- package/dist/esm/core/engine.js +1198 -0
- package/dist/esm/core/engine.js.map +1 -0
- package/dist/esm/core/index.js +16 -0
- package/dist/esm/core/index.js.map +1 -0
- package/dist/esm/index.js +87 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/interfaces/index.js +11 -0
- package/dist/esm/interfaces/index.js.map +1 -0
- package/dist/esm/layers/index.js +16 -0
- package/dist/esm/layers/index.js.map +1 -0
- package/dist/esm/layers/memory.js +384 -0
- package/dist/esm/layers/memory.js.map +1 -0
- package/dist/esm/layers/postgres.js +485 -0
- package/dist/esm/layers/postgres.js.map +1 -0
- package/dist/esm/layers/redis.js +384 -0
- package/dist/esm/layers/redis.js.map +1 -0
- package/dist/esm/types/index.js +49 -0
- package/dist/esm/types/index.js.map +1 -0
- package/dist/types/core/client.d.ts +561 -0
- package/dist/types/core/client.d.ts.map +1 -0
- package/dist/types/core/database.d.ts +130 -0
- package/dist/types/core/database.d.ts.map +1 -0
- package/dist/types/core/engine.d.ts +450 -0
- package/dist/types/core/engine.d.ts.map +1 -0
- package/dist/types/core/index.d.ts +13 -0
- package/dist/types/core/index.d.ts.map +1 -0
- package/dist/types/index.d.ts +85 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/interfaces/index.d.ts +209 -0
- package/dist/types/interfaces/index.d.ts.map +1 -0
- package/dist/types/layers/index.d.ts +16 -0
- package/dist/types/layers/index.d.ts.map +1 -0
- package/dist/types/layers/memory.d.ts +261 -0
- package/dist/types/layers/memory.d.ts.map +1 -0
- package/dist/types/layers/postgres.d.ts +313 -0
- package/dist/types/layers/postgres.d.ts.map +1 -0
- package/dist/types/layers/redis.d.ts +248 -0
- package/dist/types/layers/redis.d.ts.map +1 -0
- package/dist/types/types/index.d.ts +410 -0
- package/dist/types/types/index.d.ts.map +1 -0
- package/package.json +90 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Nuvex - PostgreSQL Storage Layer (L3)
|
|
4
|
+
* Next-gen Unified Vault Experience
|
|
5
|
+
*
|
|
6
|
+
* PostgreSQL persistent storage layer serving as the source of truth for all data.
|
|
7
|
+
* Provides ACID-compliant, durable storage with full data integrity guarantees.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - ACID-compliant persistent storage
|
|
11
|
+
* - Source of truth for all data
|
|
12
|
+
* - JSON support for flexible data structures
|
|
13
|
+
* - Automatic TTL-based expiration
|
|
14
|
+
* - Connection pooling for optimal performance
|
|
15
|
+
* - Health monitoring with SELECT 1 queries
|
|
16
|
+
*
|
|
17
|
+
* @author Waren Gonzaga, WG Technology Labs
|
|
18
|
+
* @since 2025
|
|
19
|
+
*/
|
|
20
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
21
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
22
|
+
};
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.PostgresStorage = void 0;
|
|
25
|
+
const pg_1 = __importDefault(require("pg"));
|
|
26
|
+
const { Pool } = pg_1.default;
|
|
27
|
+
const database_js_1 = require("../core/database.js");
|
|
28
|
+
/**
|
|
29
|
+
* PostgreSQL Storage Layer - L3 Persistent Storage
|
|
30
|
+
*
|
|
31
|
+
* Implements persistent storage using PostgreSQL. This is the authoritative
|
|
32
|
+
* source of truth for all data in the system. All writes must succeed here
|
|
33
|
+
* for the operation to be considered successful.
|
|
34
|
+
*
|
|
35
|
+
* **Key Features:**
|
|
36
|
+
* - ACID compliance for data integrity
|
|
37
|
+
* - Durable storage that survives restarts
|
|
38
|
+
* - JSON/JSONB support for complex objects
|
|
39
|
+
* - TTL-based automatic expiration
|
|
40
|
+
* - Connection pooling for performance
|
|
41
|
+
* - Transaction support for complex operations
|
|
42
|
+
*
|
|
43
|
+
* **Storage Schema:**
|
|
44
|
+
* - Table: nuvex_storage
|
|
45
|
+
* - Columns: id, key (unique), value (JSONB), expires_at, created_at, updated_at
|
|
46
|
+
* - Indexes: key, expires_at, key pattern (trigram)
|
|
47
|
+
*
|
|
48
|
+
* **Performance Characteristics:**
|
|
49
|
+
* - Get: O(log n) with index lookup
|
|
50
|
+
* - Set: O(log n) with index update
|
|
51
|
+
* - Latency: 5-50ms typical (storage + network)
|
|
52
|
+
*
|
|
53
|
+
* **Error Handling:**
|
|
54
|
+
* - Returns null on read errors (graceful degradation)
|
|
55
|
+
* - Logs errors for monitoring
|
|
56
|
+
* - Throws on critical connection failures
|
|
57
|
+
*
|
|
58
|
+
* @implements {StorageLayerInterface}
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* // Create PostgreSQL layer
|
|
63
|
+
* const postgres = new PostgresStorage({
|
|
64
|
+
* host: 'localhost',
|
|
65
|
+
* port: 5432,
|
|
66
|
+
* database: 'myapp',
|
|
67
|
+
* user: 'postgres',
|
|
68
|
+
* password: 'password'
|
|
69
|
+
* });
|
|
70
|
+
*
|
|
71
|
+
* // Connect (creates pool)
|
|
72
|
+
* await postgres.connect();
|
|
73
|
+
*
|
|
74
|
+
* // Store data (source of truth)
|
|
75
|
+
* await postgres.set('user:123', userData, 86400);
|
|
76
|
+
*
|
|
77
|
+
* // Retrieve data
|
|
78
|
+
* const data = await postgres.get('user:123');
|
|
79
|
+
*
|
|
80
|
+
* // Check health
|
|
81
|
+
* const isHealthy = await postgres.ping();
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* @class PostgresStorage
|
|
85
|
+
* @since 1.0.0
|
|
86
|
+
*/
|
|
87
|
+
class PostgresStorage {
|
|
88
|
+
/**
|
|
89
|
+
* Creates a new PostgresStorage instance
|
|
90
|
+
*
|
|
91
|
+
* Accepts either a PostgreSQL configuration object or an existing Pool instance.
|
|
92
|
+
* If a Pool is provided, the caller is responsible for managing its lifecycle.
|
|
93
|
+
*
|
|
94
|
+
* Schema configuration is extracted from the config object when creating a new pool.
|
|
95
|
+
* When using an existing pool, schema defaults to standard Nuvex naming.
|
|
96
|
+
*
|
|
97
|
+
* @param config - PostgreSQL configuration or existing Pool instance
|
|
98
|
+
* @param logger - Optional logger for debugging
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```typescript
|
|
102
|
+
* // With configuration (supports schema customization)
|
|
103
|
+
* const postgres = new PostgresStorage({
|
|
104
|
+
* host: 'localhost',
|
|
105
|
+
* database: 'myapp',
|
|
106
|
+
* user: 'postgres',
|
|
107
|
+
* password: 'password',
|
|
108
|
+
* schema: {
|
|
109
|
+
* tableName: 'storage_cache',
|
|
110
|
+
* columns: { key: 'key', value: 'value' }
|
|
111
|
+
* }
|
|
112
|
+
* });
|
|
113
|
+
*
|
|
114
|
+
* // With existing pool (uses default schema)
|
|
115
|
+
* const existingPool = new Pool({ ... });
|
|
116
|
+
* const postgres = new PostgresStorage(existingPool);
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
constructor(config, logger = null) {
|
|
120
|
+
this.config = config;
|
|
121
|
+
this.pool = null;
|
|
122
|
+
this.connected = false;
|
|
123
|
+
this.logger = logger;
|
|
124
|
+
// Check if config is already a Pool instance
|
|
125
|
+
this.ownsPool = !('query' in config && typeof config.query === 'function');
|
|
126
|
+
// Extract schema configuration with defaults
|
|
127
|
+
// Note: Schema is only extracted from config objects, not from existing Pool instances
|
|
128
|
+
const schema = this.ownsPool ? config.schema : undefined;
|
|
129
|
+
this.tableName = schema?.tableName ?? 'nuvex_storage';
|
|
130
|
+
this.keyColumn = schema?.columns?.key ?? 'nuvex_key';
|
|
131
|
+
this.valueColumn = schema?.columns?.value ?? 'nuvex_data';
|
|
132
|
+
// Validate all identifiers to prevent SQL injection
|
|
133
|
+
(0, database_js_1.validateSQLIdentifier)(this.tableName, 'table name');
|
|
134
|
+
(0, database_js_1.validateSQLIdentifier)(this.keyColumn, 'key column name');
|
|
135
|
+
(0, database_js_1.validateSQLIdentifier)(this.valueColumn, 'value column name');
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Establish connection to PostgreSQL
|
|
139
|
+
*
|
|
140
|
+
* Creates a connection pool (if not already provided) and tests the connection.
|
|
141
|
+
* Should be called before any storage operations.
|
|
142
|
+
*
|
|
143
|
+
* @throws {Error} If connection test fails
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```typescript
|
|
147
|
+
* try {
|
|
148
|
+
* await postgres.connect();
|
|
149
|
+
* console.log('PostgreSQL connected');
|
|
150
|
+
* } catch (error) {
|
|
151
|
+
* console.error('PostgreSQL connection failed:', error);
|
|
152
|
+
* }
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
async connect() {
|
|
156
|
+
try {
|
|
157
|
+
if (this.ownsPool) {
|
|
158
|
+
// Create new pool from config
|
|
159
|
+
this.pool = new Pool(this.config);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
// Use existing pool
|
|
163
|
+
this.pool = this.config;
|
|
164
|
+
}
|
|
165
|
+
// Test connection
|
|
166
|
+
if (this.pool) {
|
|
167
|
+
await this.pool.query('SELECT 1');
|
|
168
|
+
this.connected = true;
|
|
169
|
+
this.log('info', 'PostgreSQL L3: Connected successfully');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
this.connected = false;
|
|
174
|
+
this.log('error', 'PostgreSQL L3: Connection failed', {
|
|
175
|
+
error: error instanceof Error ? error.message : String(error)
|
|
176
|
+
});
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Close PostgreSQL connection pool
|
|
182
|
+
*
|
|
183
|
+
* Only closes the pool if we created it. If an existing pool was provided,
|
|
184
|
+
* the caller is responsible for closing it.
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```typescript
|
|
188
|
+
* await postgres.disconnect();
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
async disconnect() {
|
|
192
|
+
if (this.pool && this.ownsPool) {
|
|
193
|
+
await this.pool.end();
|
|
194
|
+
this.log('info', 'PostgreSQL L3: Disconnected');
|
|
195
|
+
}
|
|
196
|
+
this.connected = false;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Retrieve a value from PostgreSQL
|
|
200
|
+
*
|
|
201
|
+
* Queries the nuvex_storage table and automatically filters out expired entries.
|
|
202
|
+
* Deserializes the JSON-stored value.
|
|
203
|
+
*
|
|
204
|
+
* @param key - The key to retrieve
|
|
205
|
+
* @returns Promise resolving to the value or null if not found/expired
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```typescript
|
|
209
|
+
* const userData = await postgres.get('user:123');
|
|
210
|
+
* if (userData !== null) {
|
|
211
|
+
* console.log('Found in PostgreSQL');
|
|
212
|
+
* }
|
|
213
|
+
* ```
|
|
214
|
+
*/
|
|
215
|
+
async get(key) {
|
|
216
|
+
if (!this.connected || !this.pool) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
const result = await this.pool.query(`SELECT ${this.valueColumn} FROM ${this.tableName} WHERE ${this.keyColumn} = $1 AND (expires_at IS NULL OR expires_at > NOW())`, [key]);
|
|
221
|
+
if (result.rows.length === 0) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
// Value is already parsed by PostgreSQL JSONB type
|
|
225
|
+
return result.rows[0][this.valueColumn];
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
// Table might not exist yet, that's okay
|
|
229
|
+
this.log('debug', `PostgreSQL L3: Error getting key: ${key}`, {
|
|
230
|
+
error: error instanceof Error ? error.message : String(error)
|
|
231
|
+
});
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Store a value in PostgreSQL
|
|
237
|
+
*
|
|
238
|
+
* Inserts or updates the value in the nuvex_storage table. Uses UPSERT
|
|
239
|
+
* (INSERT ... ON CONFLICT) to handle existing keys efficiently.
|
|
240
|
+
*
|
|
241
|
+
* **Note:** This is the authoritative write. If this fails, the entire
|
|
242
|
+
* write operation should be considered failed.
|
|
243
|
+
*
|
|
244
|
+
* @param key - The key to store
|
|
245
|
+
* @param value - The value to store (will be JSON serialized)
|
|
246
|
+
* @param ttlSeconds - Optional TTL in seconds
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```typescript
|
|
250
|
+
* // Store with 24 hour TTL
|
|
251
|
+
* await postgres.set('user:123', userData, 86400);
|
|
252
|
+
*
|
|
253
|
+
* // Store without TTL (persists until deleted)
|
|
254
|
+
* await postgres.set('config:app', configData);
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
async set(key, value, ttlSeconds) {
|
|
258
|
+
if (!this.connected || !this.pool) {
|
|
259
|
+
this.log('warn', 'PostgreSQL L3: Cannot set - not connected', { key });
|
|
260
|
+
throw new Error('PostgreSQL not connected');
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
const expiresAt = ttlSeconds ? new Date(Date.now() + (ttlSeconds * 1000)) : null;
|
|
264
|
+
await this.pool.query(`INSERT INTO ${this.tableName} (${this.keyColumn}, ${this.valueColumn}, expires_at)
|
|
265
|
+
VALUES ($1, $2, $3)
|
|
266
|
+
ON CONFLICT (${this.keyColumn})
|
|
267
|
+
DO UPDATE SET ${this.valueColumn} = $2, expires_at = $3, updated_at = NOW()`, [key, JSON.stringify(value), expiresAt]);
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
// If table doesn't exist, this is a critical error for L3
|
|
271
|
+
this.log('error', `PostgreSQL L3: Error setting key: ${key}`, {
|
|
272
|
+
error: error instanceof Error ? error.message : String(error)
|
|
273
|
+
});
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Delete a value from PostgreSQL
|
|
279
|
+
*
|
|
280
|
+
* Permanently removes the key from the nuvex_storage table.
|
|
281
|
+
*
|
|
282
|
+
* @param key - The key to delete
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* ```typescript
|
|
286
|
+
* await postgres.delete('user:123');
|
|
287
|
+
* ```
|
|
288
|
+
*/
|
|
289
|
+
async delete(key) {
|
|
290
|
+
if (!this.connected || !this.pool) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
await this.pool.query(`DELETE FROM ${this.tableName} WHERE ${this.keyColumn} = $1`, [key]);
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
this.log('error', `PostgreSQL L3: Error deleting key: ${key}`, {
|
|
298
|
+
error: error instanceof Error ? error.message : String(error)
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Check if a key exists in PostgreSQL
|
|
304
|
+
*
|
|
305
|
+
* Queries for the key and verifies it hasn't expired.
|
|
306
|
+
*
|
|
307
|
+
* @param key - The key to check
|
|
308
|
+
* @returns Promise resolving to true if the key exists and is not expired
|
|
309
|
+
*
|
|
310
|
+
* @example
|
|
311
|
+
* ```typescript
|
|
312
|
+
* if (await postgres.exists('user:123')) {
|
|
313
|
+
* console.log('Key exists in PostgreSQL');
|
|
314
|
+
* }
|
|
315
|
+
* ```
|
|
316
|
+
*/
|
|
317
|
+
async exists(key) {
|
|
318
|
+
if (!this.connected || !this.pool) {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
const result = await this.pool.query(`SELECT 1 FROM ${this.tableName} WHERE ${this.keyColumn} = $1 AND (expires_at IS NULL OR expires_at > NOW())`, [key]);
|
|
323
|
+
return result.rows.length > 0;
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
this.log('error', `PostgreSQL L3: Error checking existence: ${key}`, {
|
|
327
|
+
error: error instanceof Error ? error.message : String(error)
|
|
328
|
+
});
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Clear all keys from PostgreSQL
|
|
334
|
+
*
|
|
335
|
+
* **WARNING:** This operation deletes all data from nuvex_storage table.
|
|
336
|
+
* Use with extreme caution in production environments.
|
|
337
|
+
*
|
|
338
|
+
* @example
|
|
339
|
+
* ```typescript
|
|
340
|
+
* await postgres.clear(); // Deletes all data
|
|
341
|
+
* ```
|
|
342
|
+
*/
|
|
343
|
+
async clear() {
|
|
344
|
+
if (!this.connected || !this.pool) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
await this.pool.query(`DELETE FROM ${this.tableName}`);
|
|
349
|
+
this.log('info', 'PostgreSQL L3: All data cleared');
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
this.log('error', 'PostgreSQL L3: Error clearing data', {
|
|
353
|
+
error: error instanceof Error ? error.message : String(error)
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Health check for PostgreSQL connection
|
|
359
|
+
*
|
|
360
|
+
* Executes a simple SELECT 1 query to verify connectivity and database
|
|
361
|
+
* responsiveness. This is a lightweight operation that tests the full
|
|
362
|
+
* connection path including pool, connection, and database.
|
|
363
|
+
*
|
|
364
|
+
* @returns Promise resolving to true if PostgreSQL is healthy and responsive
|
|
365
|
+
*
|
|
366
|
+
* @example
|
|
367
|
+
* ```typescript
|
|
368
|
+
* const isHealthy = await postgres.ping();
|
|
369
|
+
* if (!isHealthy) {
|
|
370
|
+
* console.error('PostgreSQL connection is down');
|
|
371
|
+
* }
|
|
372
|
+
* ```
|
|
373
|
+
*/
|
|
374
|
+
async ping() {
|
|
375
|
+
if (!this.connected || !this.pool) {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
let client;
|
|
379
|
+
try {
|
|
380
|
+
// Get a client from the pool and run a simple query
|
|
381
|
+
client = await this.pool.connect();
|
|
382
|
+
try {
|
|
383
|
+
await client.query('SELECT 1');
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
finally {
|
|
387
|
+
client.release();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
this.log('error', 'PostgreSQL L3: Ping failed', {
|
|
392
|
+
error: error instanceof Error ? error.message : String(error)
|
|
393
|
+
});
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Check if PostgreSQL is connected
|
|
399
|
+
*
|
|
400
|
+
* @returns True if connected
|
|
401
|
+
*/
|
|
402
|
+
isConnected() {
|
|
403
|
+
return this.connected;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Get the PostgreSQL connection pool
|
|
407
|
+
*
|
|
408
|
+
* Useful for executing custom queries or transactions.
|
|
409
|
+
*
|
|
410
|
+
* @returns The connection pool or null if not connected
|
|
411
|
+
*
|
|
412
|
+
* @example
|
|
413
|
+
* ```typescript
|
|
414
|
+
* const pool = postgres.getPool();
|
|
415
|
+
* if (pool) {
|
|
416
|
+
* const result = await pool.query('SELECT * FROM custom_table');
|
|
417
|
+
* }
|
|
418
|
+
* ```
|
|
419
|
+
*/
|
|
420
|
+
getPool() {
|
|
421
|
+
return this.pool;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Atomically increment a numeric value
|
|
425
|
+
*
|
|
426
|
+
* Uses PostgreSQL UPDATE with row-level locking for true atomic increments.
|
|
427
|
+
* If the key doesn't exist, it's created with the delta value.
|
|
428
|
+
*
|
|
429
|
+
* This operation is safe for concurrent access across multiple instances.
|
|
430
|
+
*
|
|
431
|
+
* @param key - The key to increment
|
|
432
|
+
* @param delta - The amount to increment by
|
|
433
|
+
* @param ttlSeconds - Optional TTL in seconds
|
|
434
|
+
* @returns Promise resolving to the new value after increment
|
|
435
|
+
*
|
|
436
|
+
* @example
|
|
437
|
+
* ```typescript
|
|
438
|
+
* // Atomic increment - safe for concurrent access
|
|
439
|
+
* const newValue = await postgres.increment('counter', 1, 86400);
|
|
440
|
+
* ```
|
|
441
|
+
*/
|
|
442
|
+
async increment(key, delta, ttlSeconds) {
|
|
443
|
+
if (!this.connected || !this.pool) {
|
|
444
|
+
this.log('warn', 'PostgreSQL L3: Cannot increment - not connected', { key });
|
|
445
|
+
throw new Error('PostgreSQL not connected');
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
const expiresAt = ttlSeconds ? new Date(Date.now() + (ttlSeconds * 1000)) : null;
|
|
449
|
+
// Atomic upsert to handle both insert and update cases
|
|
450
|
+
// This avoids race conditions when multiple concurrent increments happen on non-existent keys
|
|
451
|
+
const result = await this.pool.query(`INSERT INTO ${this.tableName} (${this.keyColumn}, ${this.valueColumn}, expires_at)
|
|
452
|
+
VALUES ($1, to_jsonb($2), $3)
|
|
453
|
+
ON CONFLICT (${this.keyColumn}) DO UPDATE
|
|
454
|
+
SET ${this.valueColumn} = to_jsonb(
|
|
455
|
+
CASE
|
|
456
|
+
WHEN ${this.tableName}.expires_at IS NULL OR ${this.tableName}.expires_at > NOW()
|
|
457
|
+
THEN (${this.tableName}.${this.valueColumn}::text)::numeric + (EXCLUDED.${this.valueColumn}::text)::numeric
|
|
458
|
+
ELSE (EXCLUDED.${this.valueColumn}::text)::numeric
|
|
459
|
+
END
|
|
460
|
+
),
|
|
461
|
+
expires_at = EXCLUDED.expires_at,
|
|
462
|
+
updated_at = NOW()
|
|
463
|
+
RETURNING (${this.valueColumn}::text)::numeric as value`, [key, delta, expiresAt]);
|
|
464
|
+
if (result.rows.length > 0) {
|
|
465
|
+
return Number(result.rows[0].value);
|
|
466
|
+
}
|
|
467
|
+
// Fallback: should not reach here, but return delta if no rows returned
|
|
468
|
+
return delta;
|
|
469
|
+
}
|
|
470
|
+
catch (error) {
|
|
471
|
+
this.log('error', `PostgreSQL L3: Error incrementing key: ${key}`, {
|
|
472
|
+
error: error instanceof Error ? error.message : String(error)
|
|
473
|
+
});
|
|
474
|
+
throw error;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Log a message if logger is configured
|
|
479
|
+
*
|
|
480
|
+
* @private
|
|
481
|
+
* @param level - Log level
|
|
482
|
+
* @param message - Log message
|
|
483
|
+
* @param meta - Optional metadata
|
|
484
|
+
*/
|
|
485
|
+
log(level, message, meta) {
|
|
486
|
+
if (this.logger) {
|
|
487
|
+
this.logger[level](message, meta);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
exports.PostgresStorage = PostgresStorage;
|
|
492
|
+
//# sourceMappingURL=postgres.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../../src/layers/postgres.ts"],"names":[],"mappings":"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"}
|