const { Pool } = require('pg'); const BaseStorage = require('@gofynd/fdk-extension-javascript/express/storage/base_storage'); const DEFAULT_TABLE_NAME = 'fdk session storage'; function normalizeEnvText(value) { return typeof value === 'string' ? value.trim() : ''; } function quoteIdentifier(identifier) { return `"${String(identifier).replace(/"/g, '""')}"`; } function shouldUseSsl(connectionString) { const normalized = normalizeEnvText(connectionString).toLowerCase(); if (!normalized) return false; if (normalized.includes('sslmode=disable')) return false; if (normalized.includes('sslmode=require')) return true; return !normalized.includes('localhost') && !normalized.includes('127.0.0.1'); } function getPool(connectionString) { if (!globalThis.__smsExtensionFdkStoragePool) { globalThis.__smsExtensionFdkStoragePool = new Pool({ connectionString, max: 3, idleTimeoutMillis: 30000, ssl: shouldUseSsl(connectionString) ? { rejectUnauthorized: false } : undefined, }); } return globalThis.__smsExtensionFdkStoragePool; } function serializeValue(value) { return JSON.stringify(value); } function parseValue(rawValue) { if (!rawValue) return null; if (typeof rawValue === 'object') return rawValue; try { return JSON.parse(rawValue); } catch { return null; } } class PostgresFdkStorage extends BaseStorage { constructor({ prefixKey, connectionString, tableName = DEFAULT_TABLE_NAME }) { super(prefixKey); if (!normalizeEnvText(connectionString)) { throw new Error('FDK_STORAGE_CONNECTION_STRING is required for Postgres FDK storage'); } this.pool = getPool(connectionString); this.tableIdentifier = quoteIdentifier(tableName); } buildKey(key) { return `${this.prefixKey}${key}`; } async get(key) { const storageKey = this.buildKey(key); const { rows } = await this.pool.query( `SELECT value, expires_at FROM ${this.tableIdentifier} WHERE storage_key = $1 ORDER BY updated_at DESC NULLS LAST, created_at DESC NULLS LAST, id DESC LIMIT 1`, [storageKey] ); const row = rows[0]; if (!row) return null; const expiresAt = row.expires_at ? new Date(row.expires_at) : null; if (expiresAt && expiresAt.getTime() <= Date.now()) { await this.del(key); return null; } return parseValue(row.value); } async set(key, value) { await this.writeRecord(key, value, null); return 'OK'; } async setex(key, value, ttl) { const expiresAt = Number.isFinite(ttl) ? new Date(Date.now() + ttl * 1000) : null; await this.writeRecord(key, value, expiresAt); return 'OK'; } async del(key) { const storageKey = this.buildKey(key); await this.pool.query( `DELETE FROM ${this.tableIdentifier} WHERE storage_key = $1`, [storageKey] ); } async writeRecord(key, value, expiresAt) { const storageKey = this.buildKey(key); const serializedValue = serializeValue(value); const updateResult = await this.pool.query( `UPDATE ${this.tableIdentifier} SET value = $2, expires_at = $3, updated_at = NOW() WHERE storage_key = $1`, [storageKey, serializedValue, expiresAt] ); if (updateResult.rowCount > 0) return; await this.pool.query( `INSERT INTO ${this.tableIdentifier} (storage_key, value, expires_at) VALUES ($1, $2, $3)`, [storageKey, serializedValue, expiresAt] ); } } function createFdkStorage({ prefixKey, connectionString }) { if (!normalizeEnvText(connectionString)) return null; return new PostgresFdkStorage({ prefixKey, connectionString, tableName: DEFAULT_TABLE_NAME, }); } module.exports = { DEFAULT_TABLE_NAME, PostgresFdkStorage, createFdkStorage, };