@gofynd/fdk-extension-javascript
Version:
FDK Extension Helper Library
159 lines (141 loc) • 5.63 kB
JavaScript
const BaseStorage = require('../base_storage');
const logger = require('../../logger');
// Custom Error Classes
class StorageConnectionError extends Error {
constructor(message) {
super(message);
this.name = 'StorageConnectionError';
}
}
class StorageOperationError extends Error {
constructor(message) {
super(message);
this.name = 'StorageOperationError';
}
}
/**
* Multi-level storage using ioredis and Mongoose interfaces.
* @extends BaseStorage
*/
class MultiLevelStorage extends BaseStorage {
/**
* Initializes Redis and Mongoose connections.
* @param {string} prefixKey - Prefix for all keys stored.
* @param {Object} redisInstance - ioredis instance.
* @param {Object} mongooseInstance - Mongoose connection instance.
* @param {Object} options - Additional configuration options (e.g., custom collection name, autoIndex).
*/
constructor(prefixKey, redisInstance, mongooseInstance, options = {}) {
super(prefixKey);
if (!redisInstance || !mongooseInstance) {
throw new StorageConnectionError('Both Redis and Mongoose instances are required.');
}
if (typeof redisInstance.get !== 'function') {
throw new StorageConnectionError('Invalid ioredis instance provided.');
}
this.redis = redisInstance;
this.mongoose = mongooseInstance;
const collectionName = options.collectionName || 'fdk_ext_acc_tokens';
const autoIndex = options.autoIndex !== undefined ? options.autoIndex : true;
// Support both mongoose main object and connection object
let SchemaCtor, modelFn;
if (this.mongoose.base && typeof this.mongoose.base.Schema === 'function') {
// It's a connection object
SchemaCtor = this.mongoose.base.Schema;
modelFn = this.mongoose.model.bind(this.mongoose);
} else if (typeof this.mongoose.Schema === 'function') {
// It's the main mongoose object
SchemaCtor = this.mongoose.Schema;
modelFn = this.mongoose.model.bind(this.mongoose);
} else {
throw new StorageConnectionError('Invalid Mongoose instance provided.');
}
const schema = new SchemaCtor({
key: { type: String, required: true, unique: true },
value: { type: Object, required: true },
updatedAt: { type: Date, default: Date.now },
expireAt: { type: Date, default: null, index: { expires: 0 } } // Auto-expire index
});
if (autoIndex) {
schema.index({ expireAt: 1 }, { expireAfterSeconds: 0 });
}
this.model = modelFn(collectionName, schema);
if (autoIndex) {
this.model.createIndexes().catch(err => {
logger.warn(`Error creating indexes: ${err.message}`);
});
}
}
/**
* Retrieves a value by key from Redis, falls back to Mongoose if not found.
* @param {string} key - The key to retrieve.
* @returns {Promise<Object|null>} The retrieved value or null if not found.
*/
async get(key) {
const fullKey = this.prefixKey + key;
try {
let value = await this.redis.get(fullKey);
if (value) return JSON.parse(value);
const doc = await this.model.findOne({ key: fullKey });
if (doc) {
await this.redis.set(fullKey, JSON.stringify(doc.value));
return doc.value;
}
return null;
} catch (err) {
throw new StorageOperationError(`Error retrieving key '${key}': ${err.message}`);
}
}
/**
* Sets a value for a given key in Redis and Mongoose.
* @param {string} key - The key to set.
* @param {Object} value - The value to store.
*/
async set(key, value) {
const fullKey = this.prefixKey + key;
try {
await this.redis.set(fullKey, JSON.stringify(value));
await this.model.updateOne(
{ key: fullKey },
{ value, updatedAt: Date.now() },
{ upsert: true }
);
} catch (err) {
throw new StorageOperationError(`Error setting key-value pair for '${key}': ${err.message}`);
}
}
/**
* Deletes a key from Redis and Mongoose.
* @param {string} key - The key to delete.
*/
async del(key) {
const fullKey = this.prefixKey + key;
try {
await this.redis.del(fullKey);
await this.model.deleteOne({ key: fullKey });
} catch (err) {
throw new StorageOperationError(`Error deleting key '${key}': ${err.message}`);
}
}
/**
* Sets a key-value pair with an expiration time (TTL).
* @param {string} key - The key to set.
* @param {Object} value - The value to store.
* @param {number} ttl - Time to live in seconds.
*/
async setex(key, value, ttl) {
const fullKey = this.prefixKey + key;
const expirationDate = new Date(Date.now() + ttl * 1000);
try {
await this.redis.set(fullKey, JSON.stringify(value), 'EX', ttl);
await this.model.updateOne(
{ key: fullKey },
{ value, updatedAt: Date.now(), expireAt: expirationDate },
{ upsert: true }
);
} catch (err) {
throw new StorageOperationError(`Error setting key with TTL for '${key}': ${err.message}`);
}
}
}
module.exports = MultiLevelStorage;