Skip to content

Commit

Permalink
feat(sdk-experiments) persistence from arrow functions to class, fix …
Browse files Browse the repository at this point in the history
…comments
  • Loading branch information
oidacra committed Feb 29, 2024
1 parent e733c6b commit 50e2198
Show file tree
Hide file tree
Showing 11 changed files with 211 additions and 163 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import fakeIndexedDB from 'fake-indexeddb';
import fetchMock from 'fetch-mock';

import { API_EXPERIMENTS_URL, EXPERIMENT_DB_KEY_PATH } from './constants';
import { API_EXPERIMENTS_URL, EXPERIMENT_DB_KEY_PATH, EXPERIMENT_DB_STORE_NAME } from './constants';
import { DotExperiments } from './dot-experiments';
import { IsUserIncludedResponse } from './mocks/is-user-included.mock';
import { SdkExperimentConfig } from './models';
import { getData, persistData } from './persistence/persistence';
import { SdkExperiments } from './sdk-experiments';
import { IndexDBDatabaseHandler } from './persistence/index-db-database-handler';

if (!globalThis.structuredClone) {
globalThis.structuredClone = function (obj) {
Expand All @@ -29,28 +29,33 @@ describe('SdkExperiments', () => {
status: 200
});

const instance = SdkExperiments.getInstance(configStub);
const instance = DotExperiments.getInstance(configStub);

expect(instance).toBeInstanceOf(SdkExperiments);
expect(instance).toBeInstanceOf(DotExperiments);
});

it('throws error when server returns error', async () => {
fetchMock.mock(`${configStub.server}${API_EXPERIMENTS_URL}`, 500);

try {
SdkExperiments.getInstance(configStub);
DotExperiments.getInstance(configStub);
} catch (error) {
expect(error).toEqual('HTTP error! status: 500');
}
});

it('is debug active', async () => {
const instance = SdkExperiments.getInstance(configStub);
const instance = DotExperiments.getInstance(configStub);
expect(instance.getIsDebugActive()).toBe(false);
});

it('calls persistData when persisting experiments', async () => {
const expectedData = IsUserIncludedResponse.entity;
const persistDatabase = new IndexDBDatabaseHandler({
db_store: EXPERIMENT_DB_STORE_NAME,
db_name: EXPERIMENT_DB_STORE_NAME,
db_key_path: EXPERIMENT_DB_KEY_PATH
});

Object.defineProperty(window, 'indexedDB', {
writable: true,
Expand All @@ -62,10 +67,10 @@ describe('SdkExperiments', () => {
status: 200
});

const key = await persistData(expectedData);
const key = await persistDatabase.persistData(expectedData);
expect(key).toBe(EXPERIMENT_DB_KEY_PATH);

const data = await getData();
const data = await persistDatabase.getData();
expect(data).toEqual(expectedData);
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { API_EXPERIMENTS_URL } from './constants';
import { API_EXPERIMENTS_URL, EXPERIMENT_DB_KEY_PATH, EXPERIMENT_DB_STORE_NAME } from './constants';
import { AssignedExperiments, IsUserIncludedApiResponse, SdkExperimentConfig } from './models';
import { persistData } from './persistence/persistence';
import { IndexDBDatabaseHandler } from './persistence/index-db-database-handler';
import { dotLogger } from './utils/utils';

/**
Expand All @@ -20,14 +20,14 @@ import { dotLogger } from './utils/utils';
* ```
*
* @export
* @class SdkExperiments
* @class DotExperiments
*
*/
export class SdkExperiments {
export class DotExperiments {
// TODO: make this class the entrypoint of the library
private static instance: SdkExperiments;
private static instance: DotExperiments;

constructor(private config: SdkExperimentConfig) {
private constructor(private config: SdkExperimentConfig) {
if (!this.config['server']) {
throw new Error('`server` must be provided and should not be empty!');
}
Expand All @@ -53,16 +53,20 @@ export class SdkExperiments {
* If the instance does not exist, it creates a new instance with the provided configuration and calls the `getExperimentData` method.
*
* @param {SdkExperimentConfig} config - The configuration object for initializing the SdkExperiments instance.
* @return {SdkExperiments} - The instance of the SdkExperiments class.
* @return {DotExperiments} - The instance of the SdkExperiments class.
*/
public static getInstance(config: SdkExperimentConfig): SdkExperiments {
if (!SdkExperiments.instance) {
SdkExperiments.instance = new SdkExperiments(config);
public static getInstance(config?: SdkExperimentConfig): DotExperiments {
if (!DotExperiments.instance) {
if (!config) {
throw new Error('Configuration is required to create a new instance.');
}

DotExperiments.instance = new DotExperiments(config);

this.instance.setExperimentData();
}

return SdkExperiments.instance;
return DotExperiments.instance;
}

/**
Expand Down Expand Up @@ -128,15 +132,24 @@ export class SdkExperiments {
* @private
*/
private persistExperiments(entity: AssignedExperiments) {
if (entity.experiments.length > 0) {
// TODO: Final parsed data will be stored
persistData(entity)
.then(() => {
dotLogger('Experiment data stored successfully', this.getIsDebugActive());
})
.catch((onerror) => {
dotLogger(`Error storing data. ${onerror}`, this.getIsDebugActive());
});
const indexDBDatabaseHandler = new IndexDBDatabaseHandler({
db_store: EXPERIMENT_DB_STORE_NAME,
db_name: EXPERIMENT_DB_STORE_NAME,
db_key_path: EXPERIMENT_DB_KEY_PATH
});

if (!entity.experiments.length) {
return;
}

// TODO: Final parsed data will be stored
indexDBDatabaseHandler
.persistData(entity)
.then(() => {
dotLogger('Experiment data stored successfully', this.getIsDebugActive());
})
.catch((onerror) => {
dotLogger(`Error storing data. ${onerror}`, this.getIsDebugActive());
});
}
}
2 changes: 0 additions & 2 deletions core-web/libs/sdk/experiments/src/lib/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,6 @@ export interface AssignedExperiments {
* @property {never[]} errors - An array that contains any possible error messages.
* @property {Record<string, unknown>} i18nMessagesMap - A map that contains internationalization (i18n) messages.
* @property {unknown[]} messages - An array of generic messages.
* @property {unknown | null} pagination - Pagination details for the response data, if applicable.
* @property {unknown[]} permissions - An array that contains permission data.
*/
export interface IsUserIncludedApiResponse {
entity: AssignedExperiments;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import fakeIndexedDB from 'fake-indexeddb';

import { getData, persistData } from './persistence';
import { IndexDBDatabaseHandler } from './index-db-database-handler';

import { EXPERIMENT_DB_KEY_PATH } from '../constants';
import { EXPERIMENT_DB_KEY_PATH, EXPERIMENT_DB_STORE_NAME } from '../constants';
import { IsUserIncludedResponse } from '../mocks/is-user-included.mock';

if (!globalThis.structuredClone) {
Expand All @@ -11,22 +11,30 @@ if (!globalThis.structuredClone) {
};
}

let persistDatabaseHandler: IndexDBDatabaseHandler;

beforeAll(() => {
Object.defineProperty(window, 'indexedDB', {
writable: true,
value: fakeIndexedDB
});

persistDatabaseHandler = new IndexDBDatabaseHandler({
db_store: EXPERIMENT_DB_STORE_NAME,
db_name: EXPERIMENT_DB_STORE_NAME,
db_key_path: EXPERIMENT_DB_KEY_PATH
});
});

describe('IndexedDB tests', () => {
it('saveData successfully saves data to the store', async () => {
const key = await persistData(IsUserIncludedResponse.entity);
const key = await persistDatabaseHandler.persistData(IsUserIncludedResponse.entity);
expect(key).toBe(EXPERIMENT_DB_KEY_PATH);
});

it('getDataByKey successfully retrieves data from the store', async () => {
await persistData(IsUserIncludedResponse.entity);
const data = await getData();
await persistDatabaseHandler.persistData(IsUserIncludedResponse.entity);
const data = await persistDatabaseHandler.getData();
expect(data).toEqual(IsUserIncludedResponse.entity);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
interface DbConfig {
db_name: string;
db_store: string;
db_key_path: string;
}

const DB_DEFAULT_VERSION = 1;

/**
* The `DatabaseHandler` class offers specific methods to store and get data
* from IndexedDB.
*
* @example
* // To fetch data from the IndexedDB
* const data = await DatabaseHandler.getData();
*
* @example
* // To store an object of type AssignedExperiments to IndexedDB
* await DatabaseHandler.persistData(anAssignedExperiment);
*
* @example
* // To get an object of type AssignedExperiments to IndexedDB
* await DatabaseHandler.persistData(anAssignedExperiment);
*
*/

export class IndexDBDatabaseHandler {
constructor(private config: DbConfig) {
if (!config) {
throw new Error('Config is required');
}

const { db_name, db_store, db_key_path } = config;

if (!db_name) {
throw new Error("'db_name' is required in config");
}

if (!db_store) {
throw new Error("'db_store' is required in config");
}

if (!db_key_path) {
throw new Error("'db_key_path' is required in config");
}
}

/**
* Saves the provided data to indexDB.
*
* @async
* @param {AssignedExperiments} data - The data to be saved.
* @returns {Promise<any>} - The result of the save operation.
*/
public async persistData<T>(data: T): Promise<IDBValidKey> {
const db = await this.openDB();

return await new Promise((resolve, reject) => {
const transaction = db.transaction([this.config.db_store], 'readwrite');
const store = transaction.objectStore(this.config.db_store);
const request = store.put(data, this.config.db_key_path);

request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}

/**
* Retrieves data from the database using a specific key.
*
* @async
* @returns {Promise<any>} A promise that resolves with the data retrieved from the database.
*/
public async getData<T>(): Promise<T> {
const db = await this.openDB();

return await new Promise((resolve, reject) => {
const transaction = db.transaction([this.config.db_store], 'readonly');
const store = transaction.objectStore(this.config.db_store);
const request = store.get(this.config.db_key_path);

request.onsuccess = () => resolve(request.result as T);
request.onerror = () => reject(request.error);
});
}

/**
* Builds an error message based on the provided error object.
* @param {DOMException | null} error - The error object to build the message from.
* @returns {string} The constructed error message.
*/
private getOnErrorMessage(error: DOMException | null): string {
let errorMessage =
'A database error occurred while using IndexedDB. Your browser may not support IndexedDB or IndexedDB might not be enabled.';
if (error) {
errorMessage += error.name ? ` Error Name: ${error.name}` : '';
errorMessage += error.message ? ` Error Message: ${error.message}` : '';
errorMessage += error.code ? ` Error Code: ${error.code}` : '';
}

return errorMessage;
}

/**
* Creates or opens a IndexedDB database with the specified version.
*
*
* @returns {Promise<IDBDatabase>} A promise that resolves to the opened database.
* The promise will be rejected with an error message if there was a database error.
*/
private openDB(): Promise<IDBDatabase> {
return new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open(this.config.db_name, DB_DEFAULT_VERSION);

request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
const db = this.getRequestResult(event);

if (!db.objectStoreNames.contains(this.config.db_store)) {
db.createObjectStore(this.config.db_store);
}
};

request.onerror = (event) => {
const errorMsj = this.getOnErrorMessage((event.target as IDBRequest).error);
reject(errorMsj);
};

request.onsuccess = (event: Event) => {
const db = this.getRequestResult(event);
resolve(db);
};
});
}

/**
* Retrieves the result of a database request from an Event object.
*
* @param {Event} event - The Event object containing the database request.
* @returns {IDBDatabase} - The result of the database request, casted as an IDBDatabase object.
*/
private getRequestResult(event: Event): IDBDatabase {
return (event.target as IDBRequest).result as IDBDatabase;
}
}
Loading

0 comments on commit 50e2198

Please sign in to comment.