Skip to content

Commit

Permalink
[feat] multi tenant db support
Browse files Browse the repository at this point in the history
  • Loading branch information
pkarw committed Jul 31, 2024
1 parent 461ade2 commit 7d34912
Show file tree
Hide file tree
Showing 18 changed files with 148 additions and 88 deletions.
7 changes: 4 additions & 3 deletions src/app/api/config/[key]/route.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import ServerConfigRepository from "@/data/server/server-config-repository";
import { genericDELETE } from "@/lib/generic-api";
import { genericDELETE, getDatabaseId } from "@/lib/generic-api";
import { NextRequest } from "next/server";

export async function DELETE(request: Request, { params }: { params: { key: string }} ) {
export async function DELETE(request: NextRequest, { params }: { params: { key: string }} ) {
const recordLocator = params.key;
if(!recordLocator){
return Response.json({ message: "Invalid request, no key provided within request url", status: 400 }, {status: 400});
} else {
return Response.json(await genericDELETE(request, new ServerConfigRepository(), { key: recordLocator}));
return Response.json(await genericDELETE(request, new ServerConfigRepository(getDatabaseId(request)), { key: recordLocator}));
}
}
17 changes: 9 additions & 8 deletions src/app/api/config/route.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import { ConfigDTO, configDTOSchema } from "@/data/dto";
import ServerConfigRepository from "@/data/server/server-config-repository";
import { genericDELETE, genericGET, genericPUT } from "@/lib/generic-api";
import { genericDELETE, genericGET, genericPUT, getDatabaseId } from "@/lib/generic-api";
import { NextRequest } from "next/server";

export async function PUT(request: Request) {
const apiResult = await genericPUT<ConfigDTO>(await request.json(), configDTOSchema, new ServerConfigRepository(), 'key');
export async function PUT(request: NextRequest) {
const apiResult = await genericPUT<ConfigDTO>(await request.json(), configDTOSchema, new ServerConfigRepository(getDatabaseId(request)), 'key');
return Response.json(apiResult, { status: apiResult.status });
}

export async function GET(request: Request) {
return Response.json(await genericGET<ConfigDTO>(request, new ServerConfigRepository()));
export async function GET(request: NextRequest) {
return Response.json(await genericGET<ConfigDTO>(request, new ServerConfigRepository(getDatabaseId(request))));
}

// clear all configuration
export async function DELETE(request: Request) {
const allConfigs = await genericGET<ConfigDTO>(request, new ServerConfigRepository());
export async function DELETE(request: NextRequest) {
const allConfigs = await genericGET<ConfigDTO>(request, new ServerConfigRepository(getDatabaseId(request)));
if(allConfigs.length <= 1){
return Response.json({ message: "Cannot delete the last configuration", status: 400 }, {status: 400});
} else {
const deleteResults = [];
for(const config of allConfigs){
deleteResults.push(await genericDELETE(request, new ServerConfigRepository(), { key: config.key}));
deleteResults.push(await genericDELETE(request, new ServerConfigRepository(getDatabaseId(request)), { key: config.key}));
}
return Response.json({ message: 'Configuration cleared!', data: deleteResults, status: 200 }, { status: 200 });
}
Expand Down
6 changes: 3 additions & 3 deletions src/app/api/encrypted-attachment/[id]/route.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ServerEncryptedAttachmentRepository from "@/data/server/server-encryptedattachment-repository";
import { genericDELETE } from "@/lib/generic-api";
import { genericDELETE, getDatabaseId } from "@/lib/generic-api";
import { StorageService } from "@/lib/storage-service";
import { NextResponse } from "next/server";
const storageService = new StorageService();
Expand All @@ -11,9 +11,9 @@ export async function DELETE(request: Request, { params }: { params: { id: numbe
if(!recordLocator){
return Response.json({ message: "Invalid request, no id provided within request url", status: 400 }, {status: 400});
} else {
const apiResponse = await genericDELETE(request, new ServerEncryptedAttachmentRepository(), { id: recordLocator});
const apiResponse = await genericDELETE(request, new ServerEncryptedAttachmentRepository(getDatabaseId(request)), { id: recordLocator});
if(apiResponse.status === 200){
storageService.deleteAttachment(recordLocator);
storageService.deleteAttachment(apiResponse.data.storageKey);
}
return Response.json(apiResponse);
}
Expand Down
9 changes: 5 additions & 4 deletions src/app/api/encrypted-attachment/route.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { EncryptedAttachmentDTO, EncryptedAttachmentDTOSchema } from "@/data/dto";
import ServerEncryptedAttachmentRepository from "@/data/server/server-encryptedattachment-repository";
import { genericGET, genericPUT } from "@/lib/generic-api";
import { genericGET, genericPUT, getDatabaseId } from "@/lib/generic-api";
import { StorageService } from "@/lib/storage-service";
import { getErrorMessage } from "@/lib/utils";
import { NextRequest } from "next/server";

const storageService = new StorageService();

Expand All @@ -22,7 +23,7 @@ async function handlePUTRequest(inputJson: any, request: Request, file?: File) {
let apiResult = await genericPUT<EncryptedAttachmentDTO>(
inputJson,
EncryptedAttachmentDTOSchema,
new ServerEncryptedAttachmentRepository(),
new ServerEncryptedAttachmentRepository(getDatabaseId(request)),
'id'
);
if (apiResult.status === 200) { // validation went OK, now we can store the file
Expand All @@ -41,6 +42,6 @@ async function handlePUTRequest(inputJson: any, request: Request, file?: File) {
return Response.json(apiResult, { status: apiResult.status });
}

export async function GET(request: Request) {
return Response.json(await genericGET<EncryptedAttachmentDTO>(request, new ServerEncryptedAttachmentRepository()));
export async function GET(request: NextRequest) {
return Response.json(await genericGET<EncryptedAttachmentDTO>(request, new ServerEncryptedAttachmentRepository(getDatabaseId(request))));
}
4 changes: 2 additions & 2 deletions src/app/api/keys/[key]/route.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import ServerConfigRepository from "@/data/server/server-config-repository";
import { genericDELETE } from "@/lib/generic-api";
import { genericDELETE, getDatabaseId } from "@/lib/generic-api";

export async function DELETE(request: Request, { params }: { params: { hash: string }} ) {
const recordLocator = params.hash;
if(!recordLocator){
return Response.json({ message: "Invalid request, no key provided within request url", status: 400 }, {status: 400});
} else {
return Response.json(await genericDELETE(request, new ServerConfigRepository(), { key: recordLocator}));
return Response.json(await genericDELETE(request, new ServerConfigRepository(getDatabaseId(request)), { key: recordLocator}));
}
}
13 changes: 7 additions & 6 deletions src/app/api/keys/route.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
import { ConfigDTO, configDTOSchema } from "@/data/dto";
import ServerConfigRepository from "@/data/server/server-config-repository";
import { genericDELETE, genericGET, genericPUT } from "@/lib/generic-api";
import { genericDELETE, genericGET, genericPUT, getDatabaseId } from "@/lib/generic-api";
import { NextRequest } from "next/server";

export async function PUT(request: Request) {
const apiResult = await genericPUT<ConfigDTO>(await request.json(), configDTOSchema, new ServerConfigRepository(), 'key');
const apiResult = await genericPUT<ConfigDTO>(await request.json(), configDTOSchema, new ServerConfigRepository(getDatabaseId(request)), 'key');
return Response.json(apiResult, { status: apiResult.status });
}

export async function GET(request: Request) {
return Response.json(await genericGET<ConfigDTO>(request, new ServerConfigRepository()));
export async function GET(request: NextRequest) {
return Response.json(await genericGET<ConfigDTO>(request, new ServerConfigRepository(getDatabaseId(request))));
}

// clear all configuration
export async function DELETE(request: Request) {
const allConfigs = await genericGET<ConfigDTO>(request, new ServerConfigRepository());
const allConfigs = await genericGET<ConfigDTO>(request, new ServerConfigRepository(getDatabaseId(request)));
if(allConfigs.length <= 1){
return Response.json({ message: "Cannot delete the last configuration", status: 400 }, {status: 400});
} else {
const deleteResults = [];
for(const config of allConfigs){
deleteResults.push(await genericDELETE(request, new ServerConfigRepository(), { key: config.key}));
deleteResults.push(await genericDELETE(request, new ServerConfigRepository(getDatabaseId(request)), { key: config.key}));
}
return Response.json({ message: 'Configuration cleared!', data: deleteResults, status: 200 }, { status: 200 });
}
Expand Down
6 changes: 3 additions & 3 deletions src/app/api/patient-record/route.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { PatientRecordDTO, patientRecordDTOSchema } from "@/data/dto";
import ServerPatientRecordRepository from "@/data/server/server-patientrecord-repository";
import { genericGET, genericPUT } from "@/lib/generic-api";
import { genericGET, genericPUT, getDatabaseId } from "@/lib/generic-api";
import { NextRequest } from "next/server";

export async function PUT(request: Request) {
const apiResult = await genericPUT<PatientRecordDTO>(await request.json(), patientRecordDTOSchema, new ServerPatientRecordRepository(), 'id');
const apiResult = await genericPUT<PatientRecordDTO>(await request.json(), patientRecordDTOSchema, new ServerPatientRecordRepository(getDatabaseId(request)), 'id');
return Response.json(apiResult, { status: apiResult.status });

}

export async function GET(request: NextRequest) {
return Response.json(await genericGET<PatientRecordDTO>(request, new ServerPatientRecordRepository()));
return Response.json(await genericGET<PatientRecordDTO>(request, new ServerPatientRecordRepository(getDatabaseId(request))));
}
4 changes: 2 additions & 2 deletions src/app/api/patient/[id]/route.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import ServerPatientRepository from "@/data/server/server-patient-repository";
import { genericDELETE } from "@/lib/generic-api";
import { genericDELETE, getDatabaseId } from "@/lib/generic-api";

export async function DELETE(request: Request, { params }: { params: { id: number }} ) {
const recordLocator = params.id;
if(!recordLocator){
return Response.json({ message: "Invalid request, no id provided within request url", status: 400 }, {status: 400});
} else {
return Response.json(await genericDELETE(request, new ServerPatientRepository(), { id: recordLocator}));
return Response.json(await genericDELETE(request, new ServerPatientRepository(getDatabaseId(request)), { id: recordLocator}));
}
}
17 changes: 9 additions & 8 deletions src/app/api/patient/route.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
import { PatientDTO, patientDTOSchema } from "@/data/dto";
import ServerPatientRepository from "@/data/server/server-patient-repository";
import { genericGET, genericPUT, genericDELETE } from "@/lib/generic-api";
import { genericGET, genericPUT, genericDELETE, getDatabaseId } from "@/lib/generic-api";
import { NextRequest } from "next/server";


export async function PUT(request: Request) {
const apiResult = await genericPUT<PatientDTO>(await request.json(), patientDTOSchema, new ServerPatientRepository(), 'id');
export async function PUT(request: NextRequest) {
const apiResult = await genericPUT<PatientDTO>(await request.json(), patientDTOSchema, new ServerPatientRepository(getDatabaseId(request)), 'id');
return Response.json(apiResult, { status: apiResult.status });

}

// return all patients
export async function GET(request: Request) {
return Response.json(await genericGET<PatientDTO>(request, new ServerPatientRepository()));
export async function GET(request: NextRequest) {
return Response.json(await genericGET<PatientDTO>(request, new ServerPatientRepository(getDatabaseId(request))));
}

// clear all patients
export async function DELETE(request: Request) {
const allPatients = await genericGET<PatientDTO>(request, new ServerPatientRepository());
export async function DELETE(request: NextRequest) {
const allPatients = await genericGET<PatientDTO>(request, new ServerPatientRepository(getDatabaseId(request)));
if(allPatients.length <= 1){
return Response.json({ message: "Cannot delete patients", status: 400 }, {status: 400});
} else {
const deleteResults = [];
for(const patient of allPatients){
deleteResults.push(await genericDELETE(request, new ServerPatientRepository(), { id: patient.id}));
deleteResults.push(await genericDELETE(request, new ServerPatientRepository(getDatabaseId(request)), { id: patient.id}));
}
return Response.json({ message: 'Patients cleared!', data: deleteResults, status: 200 }, { status: 200 });
}
Expand Down
6 changes: 4 additions & 2 deletions src/data/client/base-api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export type ApiEncryptionConfig = {
export class ApiClient {
private baseUrl: string;
private encryptionFilter: DTOEncryptionFilter<any> | null = null;
private encryptionConfig?: ApiEncryptionConfig = null;
private encryptionUtils: EncryptionUtils = null;
private encryptionConfig?: ApiEncryptionConfig | null = null;
private encryptionUtils: EncryptionUtils | null = null;

constructor(baseUrl: string, encryptionConfig?: ApiEncryptionConfig) {
this.baseUrl = baseUrl;
Expand Down Expand Up @@ -74,6 +74,8 @@ export class ApiClient {
}
}

headers['database-id-hash'] = 'default'; // TODO: get it from the client

const config: AxiosRequestConfig = {
method,
url: `${this.baseUrl}${endpoint}`,
Expand Down
12 changes: 12 additions & 0 deletions src/data/server/base-repository.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { BetterSQLite3Database } from "drizzle-orm/better-sqlite3";
import { Pool, pool } from "./db-provider";

// import all interfaces
export type IFilter = Record<string, any> | any;

Expand All @@ -22,6 +25,15 @@ export interface IWrite<T> {

// that class only can be extended
export abstract class BaseRepository<T> implements IWrite<T>, IRead<T> {
databaseId: string;
constructor(databaseId: string) {
this.databaseId = databaseId;
}

async db(): Promise<BetterSQLite3Database<Record<string, never>>> {
return (await pool)(this.databaseId, false);
}

async create(item: T): Promise<T> {
throw new Error("Method not implemented.");
}
Expand Down
61 changes: 37 additions & 24 deletions src/data/server/db-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,45 @@ import { getCurrentTS } from '@/lib/utils';
import fs from 'fs';

const rootPath = path.resolve(process.cwd())
export const dbFilePath = process.env.DB_FILE ?? rootPath + '/data/db.sqlite'
export let sqlite = new Database(dbFilePath);
export let db = drizzle(sqlite);

let MIGRATIONS_EXECUTED = false

export async function setup(): Promise<{ dbFilePath: string, sqlite: Database, db: BetterSQLite3Database }> {
if(!MIGRATIONS_EXECUTED) {
console.log('Running migrations')
await migrate(db, { migrationsFolder: './drizzle' }); // run the migrations
MIGRATIONS_EXECUTED = true
}
return {
dbFilePath, sqlite, db
}
}

export async function formatDb(): Promise<{ dbFilePath: string, sqlite: Database, db: BetterSQLite3Database }> {
fs.copyFileSync(rootPath + '/data/db.sqlite', rootPath + '/data/db.sqlite-' + getCurrentTS() + '.bak')
fs.unlinkSync(rootPath + '/data/db.sqlite');
export const Pool = async (maxPool = 10) => {
const databaseInstances: Record<string, BetterSQLite3Database> = {}

return async (databaseId: string, createNewDb: boolean = false) => {
if (databaseInstances[databaseId]) {
return databaseInstances[databaseId]
}

if (Object.keys(databaseInstances).length >= maxPool) {
delete databaseInstances[Object.keys(databaseInstances)[0]]
}

const databaseFile = path.join(rootPath, 'data', databaseId, 'db.sqlite')
let requiresMigration = true

sqlite = new Database(dbFilePath);
db = drizzle(sqlite);

MIGRATIONS_EXECUTED = false;
try {
fs.accessSync(databaseFile)
requiresMigration = false
} catch (error) {
if (createNewDb) {
requiresMigration = true
} else {
throw new Error('Database not found or inaccessible')
}
}

return setup();
const db = new Database(databaseFile)
databaseInstances[databaseId] = drizzle(db)

if (requiresMigration) {
console.log('Running migrations')
await migrate(databaseInstances[databaseId], { migrationsFolder: 'drizzle' })
}

return databaseInstances[databaseId]
}
}

export const pool = Pool()


8 changes: 5 additions & 3 deletions src/data/server/server-config-repository.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { BaseRepository } from "./base-repository"
import { ConfigDTO } from "../dto";
import { db } from '@/data/server/db-provider'
import { getCurrentTS } from "@/lib/utils";
import { config } from "./db-schema";
import { eq } from "drizzle-orm/sql";
Expand All @@ -11,11 +10,12 @@ export default class ServerConfigRepository extends BaseRepository<ConfigDTO> {

// create a new config
async create(item: ConfigDTO): Promise<ConfigDTO> {
return create(item, config, db); // generic implementation
return create(item, config, await this.db()); // generic implementation
}

// update config
async upsert(query:Record<string, any>, item: ConfigDTO): Promise<ConfigDTO> {
async upsert(query:Record<string, any>, item: ConfigDTO): Promise<ConfigDTO> {
const db = (await this.db());
let existingConfig = db.select({ key: config.key, value: config.value, updatedAt: config.updatedAt}).from(config).where(eq(config.key, query.key)).get() as ConfigDTO
if (!existingConfig) {
existingConfig = await this.create(item)
Expand All @@ -28,10 +28,12 @@ export default class ServerConfigRepository extends BaseRepository<ConfigDTO> {
}

async delete(query: Record<string, string>): Promise<boolean> {
const db = (await this.db());
return db.delete(config).where(eq(config.key, query.key)).run()
}

async findAll(): Promise<ConfigDTO[]> {
const db = (await this.db());
return Promise.resolve(db.select({
key: config.key,
value: config.value,
Expand Down
Loading

0 comments on commit 7d34912

Please sign in to comment.