Browse Source

add: interfaces to connect a database provider

- easy swapping between database provider
pull/1330/head
tetuaoro 10 months ago
parent
commit
07cead5bf2
  1. 68
      src/adapters/database/inmemory.ts
  2. 5
      src/composables/useDatabase.ts
  3. 15
      src/plugins/database.server.ts
  4. 46
      src/ports/database.ts
  5. 22
      src/ports/system/interface.ts
  6. 44
      src/ports/system/model.ts
  7. 55
      src/ports/types.ts
  8. 39
      src/ports/user/interface.ts
  9. 28
      src/ports/user/model.ts
  10. 8
      src/server/api/lang.get.ts
  11. 22
      src/server/databases/database.ts
  12. 86
      src/server/databases/entities/system.ts
  13. 52
      src/server/databases/entities/user.ts
  14. 12
      src/server/utils/databases/InMemory.ts
  15. 14
      src/server/utils/repositories/SystemRepository.ts
  16. 11
      src/server/utils/repositories/UserRepository.ts

68
src/server/databases/providers/inmemory.ts → src/adapters/database/inmemory.ts

@ -1,16 +1,15 @@
import type { SessionConfig } from 'h3';
import type { Identity } from '../database';
import type System from '../entities/system';
import type User from '../entities/user';
import type { UserProvider } from '../entities/user';
import type { SystemProvider } from '../entities/system';
import type DatabaseProvider from '../database';
import { ChartType } from '../entities/system';
import debug from 'debug';
import packageJson from '@/package.json';
import DatabaseProvider from '~/ports/database';
import { ChartType, Lang } from '~/ports/types';
import { ROLE } from '~/ports/user/model';
import type { SessionConfig } from 'h3';
import type { System } from '~/ports/system/model';
import type { User } from '~/ports/user/model';
import type { Identity } from '~/ports/types';
const INMDP_DEBUG = debug('InMemoryDP');
// Represent in-memory data structure
@ -20,12 +19,10 @@ type InMemoryData = {
};
// In-Memory Database Provider
export default class InMemoryDP
implements DatabaseProvider, UserProvider, SystemProvider
{
private data: InMemoryData = { users: [] };
export class InMemory extends DatabaseProvider {
protected data: InMemoryData = { users: [] };
async connect() {
override async connect() {
INMDP_DEBUG('Connection...');
const system: System = {
release: packageJson.release.version,
@ -37,7 +34,7 @@ export default class InMemoryDP
port: 51821,
webuiHost: '0.0.0.0',
sessionTimeout: 3600, // 1 hour
lang: 'en',
lang: Lang.EN,
userConfig: {
mtu: 1420,
persistentKeepalive: 0,
@ -76,13 +73,13 @@ export default class InMemoryDP
INMDP_DEBUG('Connection done');
}
async disconnect() {
override async disconnect() {
this.data = { users: [] };
}
async getSystem() {
override async getSystem() {
INMDP_DEBUG('Get System');
return this.data.system || null;
return this.data.system;
}
async saveSystem(system: System) {
@ -90,11 +87,15 @@ export default class InMemoryDP
this.data.system = system;
}
async getUsers() {
override async getLang() {
return this.data.system?.lang || Lang.EN;
}
override async getUsers() {
return this.data.users;
}
async getUser(id: Identity<User>) {
override async getUser(id: Identity<User>) {
INMDP_DEBUG('Get User');
if (typeof id === 'string') {
return this.data.users.find((user) => user.id === id);
@ -102,14 +103,37 @@ export default class InMemoryDP
return this.data.users.find((user) => user.id === id.id);
}
async saveUser(user: User) {
override async saveUser(user: User) {
let _user = await this.getUser(user);
if (_user) {
INMDP_DEBUG('Update User');
_user = user;
} else {
INMDP_DEBUG('New User');
if (this.data.users.length == 0) {
// first user is admin
user.role = ROLE.ADMIN;
}
this.data.users.push(user);
}
}
override async deleteUser(id: Identity<User>) {
const _id = typeof id === 'string' ? id : id.id;
const idx = this.data.users.findIndex((user) => user.id == _id);
if (idx !== -1) {
this.data.users.splice(idx, 1);
}
}
}
export default function initInMemoryProvider() {
const provider = new InMemory();
provider.connect().catch((err) => {
console.error(err);
process.exit(1);
});
return provider;
}

5
src/composables/useDatabase.ts

@ -0,0 +1,5 @@
import type { InMemory } from '~/adapters/database/inmemory';
export default (): InMemory => {
return useNuxtApp().$database;
};

15
src/plugins/database.server.ts

@ -0,0 +1,15 @@
/**
* Changing the Database Provider
* This design allows for easy swapping of different database implementations.
*
*/
import initInMemoryProvider from '~/adapters/database/inmemory';
export default defineNuxtPlugin(() => {
return {
provide: {
database: initInMemoryProvider(),
},
};
});

46
src/ports/database.ts

@ -0,0 +1,46 @@
import type SystemRepository from './system/interface';
import type UserRepository from './user/interface';
import type { Identity, Undefined, Lang } from './types';
import type { User } from './user/model';
import type { System } from './system/model';
/**
* Abstract class for database operations.
* Provides methods to connect, disconnect, and interact with system and user data.
*/
export default abstract class DatabaseProvider
implements SystemRepository, UserRepository
{
/**
* Connects to the database.
*/
connect(): Promise<void> {
throw new Error('Method not implemented.');
}
/**
* Disconnects from the database.
*/
disconnect(): Promise<void> {
throw new Error('Method not implemented.');
}
getSystem(): Promise<System | Undefined> {
throw new Error('Method not implemented.');
}
getLang(): Promise<Lang> {
throw new Error('Method not implemented.');
}
getUsers(): Promise<Array<User>> {
throw new Error('Method not implemented.');
}
getUser(_id: Identity<User>): Promise<User | Undefined> {
throw new Error('Method not implemented.');
}
saveUser(_user: User): Promise<void> {
throw new Error('Method not implemented.');
}
deleteUser(_id: Identity<User>): Promise<void> {
throw new Error('Method not implemented.');
}
}

22
src/ports/system/interface.ts

@ -0,0 +1,22 @@
import type { Lang, Undefined } from '../types';
import type { System } from './model';
/**
* Interface for system-related database operations.
* This interface provides methods for retrieving system configuration data
* and specific system properties, such as the language setting, from the database.
*/
export default interface SystemRepository {
/**
* Retrieves the system configuration data from the database.
* @returns {Promise<System | Undefined>} A promise that resolves to the system data
* if found, or `undefined` if the system data is not available.
*/
getSystem(): Promise<System | Undefined>;
/**
* Retrieves the system's language setting.
* @returns {Promise<Lang>} The current language setting of the system.
*/
getLang(): Promise<Lang>;
}

44
src/ports/system/model.ts

@ -0,0 +1,44 @@
import type { SessionConfig } from 'h3';
import type {
Address,
IpTables,
Lang,
Port,
Prometheus,
SessionTimeOut,
String,
Boolean,
TrafficStats,
Version,
WGConfig,
WGInterface,
} from '../types';
/**
* Representing the WireGuard network configuration data structure of a computer interface system.
*/
export type System = {
interface: WGInterface;
release: Version;
port: number;
webuiHost: String;
// maxAge
sessionTimeout: SessionTimeOut;
lang: Lang;
userConfig: WGConfig;
wgPath: String;
wgDevice: String;
wgHost: Address;
wgPort: Port;
wgConfigPort: Port;
iptables: IpTables;
trafficStats: TrafficStats;
wgEnableExpiresTime: Boolean;
prometheus: Prometheus;
sessionConfig: SessionConfig;
};

55
src/ports/types.ts

@ -0,0 +1,55 @@
export type Undefined = null | undefined | 0 | '';
export enum Lang {
/* english */
EN = 'en',
/* french */
FR = 'fr',
}
export type String = string;
export type ID = String;
export type Boolean = boolean;
export type Version = String;
export type SessionTimeOut = number;
export type Port = number;
export type Address = String;
export type HashPassword = String;
export type Command = String;
export type Key = String;
export type IpTables = {
wgPreUp: Command;
wgPostUp: Command;
wgPreDown: Command;
wgPostDown: Command;
};
export type WGInterface = {
privateKey: Key;
publicKey: Key;
address: Address;
};
export type WGConfig = {
mtu: number;
persistentKeepalive: number;
rangeAddress: Address;
defaultDns: Array<Address>;
allowedIps: Array<Address>;
};
export enum ChartType {
None = 0,
Line = 1,
Area = 2,
Bar = 3,
}
export type TrafficStats = {
enabled: boolean;
type: ChartType;
};
export type Prometheus = {
enabled: boolean;
password?: HashPassword | Undefined;
};
/**
* `id` of T or T.
*
* @template T - The specific type that can be used in place of id String.
*/
export type Identity<T> = ID | T;

39
src/ports/user/interface.ts

@ -0,0 +1,39 @@
import type { Identity, Undefined } from '../types';
import type { User } from './model';
/**
* Interface for user-related database operations.
* This interface provides methods for managing user data.
*/
export default interface UserRepository {
/**
* Retrieves all users from the database.
* @returns {Promise<Array<User>>} A array of users data.
*/
getUsers(): Promise<Array<User>>;
/**
* Retrieves a user by their ID or User object from the database.
* @param {Identity<User>} id - The ID of the user or a User object.
* @returns {Promise<User | Undefined>} A promise that resolves to the user data
* if found, or `undefined` if the user is not available.
*/
getUser(id: Identity<User>): Promise<User | Undefined>;
/**
* Creates or updates a user in the database.
* @param {User} user - The user to be saved.
*
* **Note:** If the user already exists, this method will update their details.
* If the user does not exist, it will create a new user entry.
* @returns {Promise<void>} A promise that resolves when the operation is complete.
*/
saveUser(user: User): Promise<void>;
/**
* Deletes a user from the database.
* @param {Identity<User>} id - The ID of the user or a User object to be deleted.
* @returns {Promise<void>} A promise that resolves when the user has been deleted.
*/
deleteUser(id: Identity<User>): Promise<void>;
}

28
src/ports/user/model.ts

@ -0,0 +1,28 @@
import type { Address, ID, Key, HashPassword, String } from '../types';
export enum ROLE {
/* Full permissions to any resources (app, database...) */
ADMIN = 'ADMIN',
/* Grants write and read permissions on their own resources and `CLIENT` resources without `ADMIN` permissions */
EDITOR = 'EDITOR',
/* Grants write and read permissions on their own resources */
CLIENT = 'CLIENT',
}
/**
* Representing a user data structure.
*/
export type User = {
id: ID;
role: ROLE;
username: String;
password: HashPassword;
name: String;
address: Address;
privateKey: Key;
publicKey: Key;
preSharedKey: String;
createdAt: Date;
updatedAt: Date;
enabled: boolean;
};

8
src/server/api/lang.get.ts

@ -1,7 +1,7 @@
import useDatabase from '~/composables/useDatabase';
export default defineEventHandler(async (event) => {
setHeader(event, 'Content-Type', 'application/json');
const provider = InMemory; // TODO multiple provider
const lang = await SystemRepository.getLang(provider);
return lang;
const db = useDatabase();
return db.getLang();
});

22
src/server/databases/database.ts

@ -1,22 +0,0 @@
export type Undefined = null | undefined | 0 | '';
/**
* `id` of T or T.
*
* @template T - The specific type that can be used in place of id string.
*/
export type Identity<T> = string | T;
/**
* Abstract class for database operations.
* Provides methods to connect, disconnect, and interact with system and user data.
*/
export default abstract class DatabaseProvider {
/**
* Connects to the database.
*/
abstract connect(): Promise<void>;
/**
* Disconnects from the database.
*/
abstract disconnect(): Promise<void>;
}

86
src/server/databases/entities/system.ts

@ -1,86 +0,0 @@
import type { SessionConfig } from 'h3';
import type { Undefined } from '../database';
export type Lang = 'en' | 'ua' | 'ru' | 'tr' | 'no' | 'pl' | 'fr';
export type Version = string;
export type SessionTimeOut = number;
export type Port = number;
export type Address = string;
export type PassordHash = string;
export type Command = string;
export type Key = string;
export type IpTables = {
wgPreUp: Command;
wgPostUp: Command;
wgPreDown: Command;
wgPostDown: Command;
};
export type WGInterface = {
privateKey: Key;
publicKey: Key;
address: Address;
};
export type WGConfig = {
mtu: number;
persistentKeepalive: number;
rangeAddress: Address;
defaultDns: Array<Address>;
allowedIps: Array<Address>;
};
export enum ChartType {
None = 0,
Line = 1,
Area = 2,
Bar = 3,
}
export type TrafficStats = {
enabled: boolean;
type: ChartType;
};
export type Prometheus = {
enabled: boolean;
password?: PassordHash | Undefined;
};
/**
* Representing the WireGuard network configuration data structure of a computer interface system.
*/
type System = {
interface: WGInterface;
release: Version;
port: number;
webuiHost: string;
// maxAge
sessionTimeout: SessionTimeOut;
lang: Lang;
userConfig: WGConfig;
wgPath: string;
wgDevice: string;
wgHost: Address;
wgPort: Port;
wgConfigPort: Port;
iptables: IpTables;
trafficStats: TrafficStats;
wgEnableExpiresTime: boolean;
prometheus: Prometheus;
sessionConfig: SessionConfig;
};
export default System;
/**
* Abstract class for system-related database operations.
* This class provides a method for retrieving system configuration data from the database.
*/
export abstract class SystemProvider {
/**
* Retrieves system data from the database.
* @returns {Promise<System | Undefined>} The system data or null if not found.
*/
abstract getSystem(): Promise<System | Undefined>;
}

52
src/server/databases/entities/user.ts

@ -1,52 +0,0 @@
import type { Identity, Undefined } from '../database';
import type { Address, Key, PassordHash } from './system';
export type ROLE = 'ADMIN';
/**
* Representing a user data structure.
*/
type User = {
id: string;
roles: Array<ROLE>;
username: string;
password: PassordHash;
name: string;
address: Address;
privateKey: Key;
publicKey: Key;
preSharedKey: string;
createdAt: Date;
updatedAt: Date;
enabled: boolean;
};
export default User;
/**
* Abstract class for user-related database operations.
* This class provides methods for retrieving and saving user data.
*/
export abstract class UserProvider {
/**
* Retrieves all users from the database.
* @returns {Promise<Array<User>>} The array of users data.
*/
abstract getUsers(): Promise<Array<User>>;
/**
* Retrieves a user by their ID or by User structure from the database.
* @param {Identity<User>} id - The ID of the user or a user.
* @returns {Promise<User | Undefined>} The user data or null if not found.
*/
abstract getUser(id: Identity<User>): Promise<User | Undefined>;
/**
* Creates or Updates a user in the database.
* @param {User} user - The user to be saved.
*
* **Note:** If the user already exists, this method will update their details.
* If the user does not exist, it will create a new user entry.
*/
abstract saveUser(user: User): Promise<void>;
}

12
src/server/utils/databases/InMemory.ts

@ -1,12 +0,0 @@
import InMemoryDP from '@/server/databases/providers/inmemory';
const provider = new InMemoryDP(); // TODO multiple providers
provider.connect().catch((err) => {
console.error(err);
provider.disconnect().catch((err) => {
console.error(err);
});
});
export default provider;

14
src/server/utils/repositories/SystemRepository.ts

@ -1,14 +0,0 @@
import type { Lang, SystemProvider } from '@/server/databases/entities/system';
class SystemRepository {
async getLang(provider: SystemProvider): Promise<Lang> {
const _system = await provider.getSystem();
if (_system) {
const { lang } = _system;
return lang;
}
return 'en';
}
}
export default new SystemRepository();

11
src/server/utils/repositories/UserRepository.ts

@ -1,11 +0,0 @@
import type User from '@/server/databases/entities/user';
import type { UserProvider } from '@/server/databases/entities/user';
import type { Undefined } from '@/server/databases/database';
class UserRepository {
newUser(db: UserProvider): Promise<User | Undefined> {
return db.getUser('');
}
}
export default new UserRepository();
Loading…
Cancel
Save