Skip to content

Commit

Permalink
Added plugin support
Browse files Browse the repository at this point in the history
  • Loading branch information
thehenrytsai committed Jul 9, 2024
1 parent 24199fa commit c1078c6
Show file tree
Hide file tree
Showing 10 changed files with 325 additions and 63 deletions.
4 changes: 2 additions & 2 deletions src/dwn-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { DwnServerConfig } from './config.js';
import log from 'loglevel';
import prefix from 'loglevel-plugin-prefix';
import { config as defaultConfig } from './config.js';
import { getDWNConfig } from './storage.js';
import { getDwnConfig } from './storage.js';
import { HttpServerShutdownHandler } from './lib/http-server-shutdown-handler.js';
import { HttpApi } from './http-api.js';
import { RegistrationManager } from './registration/registration-manager.js';
Expand Down Expand Up @@ -105,7 +105,7 @@ export class DwnServer {
eventStream = new EventEmitterStream();
}

const dwnConfig = getDWNConfig(this.config, {
const dwnConfig = await getDwnConfig(this.config, {
didResolver: this.didResolver,
tenantGate: registrationManager,
eventStream,
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ export { DwnServerConfig } from './config.js';
export { DwnServer, DwnServerOptions } from './dwn-server.js';
export { HttpApi } from './http-api.js';
export { jsonRpcRouter } from './json-rpc-api.js';
export { EStoreType, BackendTypes, StoreType } from './storage.js';
export { StoreType, BackendTypes, DwnStore } from './storage.js';
export { WsApi } from './ws-api.js';
18 changes: 18 additions & 0 deletions src/pluginLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Dynamically loads a plugin from a file path by invoking the argument-less constructor of the default exported class.
*/
export async function loadPlugin<T>(filePath: string): Promise<T> {
try {
const module = await import(filePath);

// Check if the default export is a class
if (typeof module.default === 'function') {
const instance: T = new module.default() as T;
return instance;
} else {
throw new Error(`Default export at ${filePath} is not a class.`);
}
} catch (error) {
throw new Error(`Failed to load component at ${filePath}: ${error.message}`);
}
}
4 changes: 2 additions & 2 deletions src/registration/registration-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { RegistrationData, RegistrationRequest } from "./registration-types
import type { ProofOfWorkChallengeModel } from "./proof-of-work-types.js";
import { DwnServerError, DwnServerErrorCode } from "../dwn-error.js";
import type { ActiveTenantCheckResult, TenantGate } from "@tbd54566975/dwn-sdk-js";
import { getDialectFromURI } from "../storage.js";
import { getDialectFromUrl } from "../storage.js";
import { readFileSync } from "fs";

/**
Expand Down Expand Up @@ -77,7 +77,7 @@ export class RegistrationManager implements TenantGate {
});

// Initialize RegistrationStore.
const sqlDialect = getDialectFromURI(new URL(registrationStoreUrl));
const sqlDialect = getDialectFromUrl(new URL(registrationStoreUrl));
const registrationStore = await RegistrationStore.create(sqlDialect);
registrationManager.registrationStore = registrationStore;

Expand Down
119 changes: 72 additions & 47 deletions src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ import pg from 'pg';
import Cursor from 'pg-cursor';

import type { DwnServerConfig } from './config.js';
import { loadPlugin } from './pluginLoader.js';

export enum EStoreType {
export enum StoreType {
DataStore,
MessageStore,
EventLog,
Expand All @@ -48,44 +49,44 @@ export enum BackendTypes {
POSTGRES = 'postgres',
}

export type StoreType = DataStore | EventLog | MessageStore | ResumableTaskStore;
export type DwnStore = DataStore | EventLog | MessageStore | ResumableTaskStore;

export function getDWNConfig(
export async function getDwnConfig(
config : DwnServerConfig,
options : {
didResolver? : DidResolver,
tenantGate? : TenantGate,
eventStream? : EventStream,
}
): DwnConfig {
): Promise<DwnConfig> {
const { tenantGate, eventStream, didResolver } = options;
const dataStore: DataStore = getStore(config.dataStore, EStoreType.DataStore);
const eventLog: EventLog = getStore(config.eventLog, EStoreType.EventLog);
const messageStore: MessageStore = getStore(config.messageStore, EStoreType.MessageStore);
const resumableTaskStore: ResumableTaskStore = getStore(config.messageStore, EStoreType.ResumableTaskStore);
const dataStore: DataStore = await getStore(config.dataStore, StoreType.DataStore);
const eventLog: EventLog = await getStore(config.eventLog, StoreType.EventLog);
const messageStore: MessageStore = await getStore(config.messageStore, StoreType.MessageStore);
const resumableTaskStore: ResumableTaskStore = await getStore(config.messageStore, StoreType.ResumableTaskStore);

return { didResolver, eventStream, eventLog, dataStore, messageStore, resumableTaskStore, tenantGate };
}

function getLevelStore(
storeURI: URL,
storeType: EStoreType,
): DataStore | MessageStore | EventLog | ResumableTaskStore {
storeType: StoreType,
): DwnStore {
switch (storeType) {
case EStoreType.DataStore:
case StoreType.DataStore:
return new DataStoreLevel({
blockstoreLocation: storeURI.host + storeURI.pathname + '/DATASTORE',
});
case EStoreType.MessageStore:
case StoreType.MessageStore:
return new MessageStoreLevel({
blockstoreLocation: storeURI.host + storeURI.pathname + '/MESSAGESTORE',
indexLocation: storeURI.host + storeURI.pathname + '/INDEX',
});
case EStoreType.EventLog:
case StoreType.EventLog:
return new EventLogLevel({
location: storeURI.host + storeURI.pathname + '/EVENTLOG',
});
case EStoreType.ResumableTaskStore:
case StoreType.ResumableTaskStore:
return new ResumableTaskStoreLevel({
location: storeURI.host + storeURI.pathname + '/RESUMABLE-TASK-STORE',
});
Expand All @@ -94,42 +95,45 @@ function getLevelStore(
}
}

function getDBStore(
db: Dialect,
storeType: EStoreType,
): DataStore | MessageStore | EventLog | ResumableTaskStore {
function getSqlStore(
connectionUrl: URL,
storeType: StoreType,
): DwnStore {
const dialect = getDialectFromUrl(connectionUrl);

switch (storeType) {
case EStoreType.DataStore:
return new DataStoreSql(db);
case EStoreType.MessageStore:
return new MessageStoreSql(db);
case EStoreType.EventLog:
return new EventLogSql(db);
case EStoreType.ResumableTaskStore:
return new ResumableTaskStoreSql(db);
case StoreType.DataStore:
return new DataStoreSql(dialect);
case StoreType.MessageStore:
return new MessageStoreSql(dialect);
case StoreType.EventLog:
return new EventLogSql(dialect);
case StoreType.ResumableTaskStore:
return new ResumableTaskStoreSql(dialect);
default:
throw new Error('Unexpected db store type');
throw new Error(`Unsupported store type ${storeType} for SQL store.`);
}
}

function getStore(
storeString: string,
storeType: EStoreType.DataStore,
): DataStore;
function getStore(
storeString: string,
storeType: EStoreType.EventLog,
): EventLog;
function getStore(
storeString: string,
storeType: EStoreType.MessageStore,
): MessageStore;
function getStore(
storeString: string,
storeType: EStoreType.ResumableTaskStore,
): ResumableTaskStore;
function getStore(storeString: string, storeType: EStoreType): StoreType {
const storeURI = new URL(storeString);
/**
* Check if the given string is a file path.
*/
function isFilePath(configString: string): boolean {
const filePathPrefixes = ['/', './', '../'];
return filePathPrefixes.some(prefix => configString.startsWith(prefix));
}

async function getStore(storeString: string, storeType: StoreType.DataStore): Promise<DataStore>;
async function getStore(storeString: string, storeType: StoreType.EventLog): Promise<EventLog>;
async function getStore(storeString: string, storeType: StoreType.MessageStore): Promise<MessageStore>;
async function getStore(storeString: string, storeType: StoreType.ResumableTaskStore): Promise<ResumableTaskStore>;
async function getStore(storeConfigString: string, storeType: StoreType): Promise<DwnStore> {
if (isFilePath(storeConfigString)) {
return await loadStoreFromFilePath(storeConfigString, storeType);
}
// else treat the `storeConfigString` as a connection string

const storeURI = new URL(storeConfigString);

switch (storeURI.protocol.slice(0, -1)) {
case BackendTypes.LEVEL:
Expand All @@ -138,14 +142,35 @@ function getStore(storeString: string, storeType: EStoreType): StoreType {
case BackendTypes.SQLITE:
case BackendTypes.MYSQL:
case BackendTypes.POSTGRES:
return getDBStore(getDialectFromURI(storeURI), storeType);
return getSqlStore(storeURI, storeType);

default:
throw invalidStorageSchemeMessage(storeURI.protocol);
}
}

export function getDialectFromURI(connectionUrl: URL): Dialect {
/**
* Loads a DWN store plugin of the given type from the given file path.
*/
async function loadStoreFromFilePath(
filePath: string,
storeType: StoreType,
): Promise<DwnStore> {
switch (storeType) {
case StoreType.DataStore:
return await loadPlugin<DataStore>(filePath);
case StoreType.EventLog:
return await loadPlugin<EventLog>(filePath);
case StoreType.MessageStore:
return await loadPlugin<MessageStore>(filePath);
case StoreType.ResumableTaskStore:
return await loadPlugin<ResumableTaskStore>(filePath);
default:
throw new Error(`Loading store for unsupported store type ${storeType} from path ${filePath}`);
}
}

export function getDialectFromUrl(connectionUrl: URL): Dialect {
switch (connectionUrl.protocol.slice(0, -1)) {
case BackendTypes.SQLITE:
const path = connectionUrl.host + connectionUrl.pathname;
Expand Down
4 changes: 2 additions & 2 deletions src/web5-connect/web5-connect-server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getDialectFromURI } from "../storage.js";
import { getDialectFromUrl } from "../storage.js";
import { randomUuid } from '@web5/crypto/utils';
import { SqlTtlCache } from "./sql-ttl-cache.js";

Expand Down Expand Up @@ -49,7 +49,7 @@ export class Web5ConnectServer {
const web5ConnectServer = new Web5ConnectServer({ baseUrl });

// Initialize TTL cache.
const sqlDialect = getDialectFromURI(new URL(sqlTtlCacheUrl));
const sqlDialect = getDialectFromUrl(new URL(sqlTtlCacheUrl));
web5ConnectServer.cache = await SqlTtlCache.create(sqlDialect);

return web5ConnectServer;
Expand Down
1 change: 0 additions & 1 deletion tests/http-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ describe('http api', function () {

before(async function () {
clock = useFakeTimers({ shouldAdvanceTime: true });

config.registrationStoreUrl = 'sqlite://';
config.registrationProofOfWorkEnabled = true;
config.termsOfServiceFilePath = './tests/fixtures/terms-of-service.txt';
Expand Down
26 changes: 26 additions & 0 deletions tests/plugins/data-store-sqlite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { DataStore } from "@tbd54566975/dwn-sdk-js";
import { DataStoreSql } from "@tbd54566975/dwn-sql-store";
import { getDialectFromUrl } from "../../src/storage.js";

/**
* An example of a DataStore plugin that is used for testing.
* The points to note are:
* - The class must be a default export.
* - The constructor must not take any arguments.
*/
export default class DataStoreSqlite extends DataStoreSql implements DataStore {
constructor() {
const sqliteDialect = getDialectFromUrl(new URL('sqlite://'));
super(sqliteDialect);

// NOTE: the following line is added purely to test the constructor invocation.
DataStoreSqlite.spyingTheConstructor();
}

/**
* NOTE: This method is introduced purely to indirectly test/spy invocation of the constructor.
* As I was unable to find an easy way to directly spy the constructor.
*/
public static spyingTheConstructor(): void {
}
}
Loading

0 comments on commit c1078c6

Please sign in to comment.