Skip to content

Commit

Permalink
feature: first pass at a database repository interface
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon Carlson committed Dec 5, 2024
1 parent 52d1c48 commit ca6dd50
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 90 deletions.
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ NEXTAUTH_URL=http://localhost/meditor/api/auth
NEXTAUTH_URL_INTERNAL=http://meditor_app:3000/meditor/api/auth
AUTH_SECRET=ThisSecretEncryptsTheAuthJWTToken

DB_CONNECTION=mongo
DB_HOST=localhost
DB_PORT=27017
DB_DATABASE=meditor

# include in your .env to disable email notifications, if disabled you will see the email in the logs instead of an actual
# email being sent
DISABLE_EMAIL_NOTIFICATIONS=true
Expand Down
58 changes: 58 additions & 0 deletions packages/app/database/repositories/mongo.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { DatabaseRepositoryInterface } from 'database/types'
import { Db, Document, MongoClient, WithId } from 'mongodb'

export class MongoRepository<T> implements DatabaseRepositoryInterface<T> {
#client: MongoClient
#db: Db

constructor(client: MongoClient, dbName?: string) {
this.#client = client
this.#db = this.#client.db(dbName ?? process.env.DB_NAME)
}

getDb() {
// TODO: remove
return this.#db
}

async findAll(collection: string): Promise<T[]> {
const models = await this.#db
.collection(collection)
.aggregate(
[
{ $sort: { 'x-meditor.modifiedOn': -1 } }, // Sort descending by version (date)
{ $group: { _id: '$name', doc: { $first: '$$ROOT' } } }, // Grab all fields in the most recent version
{ $replaceRoot: { newRoot: '$doc' } }, // Put all fields of the most recent doc back into root of the document
],
{ allowDiskUse: true }
)
.toArray()

return this.#makeSafeObjectIDs(models)
}

async find(
collection: string,
title: string,
titleProperty: string = 'title'
): Promise<T> {
const models = await this.#db
.collection(collection)
.find({ [titleProperty]: title })
.sort({ 'x-meditor.modifiedOn': -1 })
.limit(1)
.toArray()

return this.#makeSafeObjectIDs(models)[0]
}

/**
* Next doesn't know how to process the Mongo _id property, as it's an object, not a string. So this hack parses ahead of time
* https://github.com/vercel/next.js/issues/11993
*/
#makeSafeObjectIDs(
records: Record<string, any> | Record<string, any>[] | WithId<Document> | null
) {
return !!records ? JSON.parse(JSON.stringify(records)) : records
}
}
46 changes: 46 additions & 0 deletions packages/app/database/repository.factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { DatabaseRepositoryInterface } from './types'
import { MongoRepository } from './repositories/mongo.repository'
import log from 'lib/log'
import { MongoClient } from 'mongodb'

export async function getDatabaseRepository<T>(): Promise<
DatabaseRepositoryInterface<T>
> {
const type = process.env.DB_CONNECTION ?? 'mongo'

if (type === 'mongo') {
const client = await getMongoClient()
return new MongoRepository<T>(client)
}

throw new Error(`Unsupported repository type: ${type}`)
}

async function getMongoClient(): Promise<MongoClient> {
const uri = `mongodb://${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_DATABASE}`

let mongoClient: MongoClient
let mongoClientPromise: Promise<MongoClient>

if (process.env.NODE_ENV === 'development') {
// In development mode, use a global variable so that the value
// is preserved across module reloads caused by HMR (Hot Module Replacement).
if (!globalThis._mongoClientPromise) {
log.info('Connecting to MongoDB (DEV): ', uri)

mongoClient = new MongoClient(uri)
// @ts-ignore in development
global._mongoClientPromise = mongoClient.connect()
}
// @ts-ignore in development
mongoClientPromise = global._mongoClientPromise
} else {
log.info('Connecting to MongoDB: ', uri)

// In production mode, it's best to not use a global variable.
mongoClient = new MongoClient(uri)
mongoClientPromise = mongoClient.connect()
}

return mongoClientPromise
}
4 changes: 4 additions & 0 deletions packages/app/database/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface DatabaseRepositoryInterface<T> {
findAll(collection: string): Promise<T[]>
find(collection: string, title: string, titleProperty?: string): Promise<T>
}
40 changes: 8 additions & 32 deletions packages/app/lib/mongodb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,16 @@
* https://github.com/vercel/next.js/tree/canary/examples/with-mongodb
*/

import { Document, MongoClient, WithId } from 'mongodb'
import log from './log'
import { getDatabaseRepository } from 'database/repository.factory'
import { Document, WithId } from 'mongodb'

const uri =
(process.env.MONGO_URL ||
process.env.MONGOURL ||
'mongodb://meditor_database:27017/') + 'meditor'

let mongoClient: MongoClient

let mongoClientPromise: Promise<MongoClient>

if (process.env.NODE_ENV === 'development') {
// In development mode, use a global variable so that the value
// is preserved across module reloads caused by HMR (Hot Module Replacement).
if (!globalThis._mongoClientPromise) {
log.info('Connecting to MongoDB (DEV): ', uri)

mongoClient = new MongoClient(uri)
// @ts-ignore in development
global._mongoClientPromise = mongoClient.connect()
}
// @ts-ignore in development
mongoClientPromise = global._mongoClientPromise
} else {
log.info('Connecting to MongoDB: ', uri)

// In production mode, it's best to not use a global variable.
mongoClient = new MongoClient(uri)
mongoClientPromise = mongoClient.connect()
}
// TODO: REMOVE THIS WHOLE FILE

const getDb = async (dbName?: string) => {
return (await mongoClientPromise).db(dbName || process.env.DB_NAME)
const repository = await getDatabaseRepository()

// @ts-expect-error
return repository.db
}

// Next doesn't know how to process the Mongo _id property, as it's an object, not a string. So this hack parses ahead of time
Expand All @@ -47,4 +23,4 @@ function makeSafeObjectIDs(
return !!records ? JSON.parse(JSON.stringify(records)) : records
}

export { getDb as default, makeSafeObjectIDs, mongoClientPromise }
export { getDb as default, makeSafeObjectIDs }
53 changes: 0 additions & 53 deletions packages/app/models/db.ts

This file was deleted.

10 changes: 5 additions & 5 deletions packages/app/models/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { runModelTemplates } from '../macros/service'
import { ErrorCode, HttpException } from '../utils/errors'
import { isJson } from '../utils/jsonschema-validate'
import { getWorkflowByDocumentState } from '../workflows/service'
import { getModelsDb } from './db'
import type { Model, ModelWithWorkflow } from './types'
import { getDatabaseRepository } from 'database/repository.factory'

const MODELS_REQUIRING_AUTHENTICATION = ['Users']

Expand Down Expand Up @@ -36,8 +36,8 @@ export async function getModel(
throw new HttpException(ErrorCode.BadRequest, 'Model name is required')
}

const modelsDb = await getModelsDb()
const model = await modelsDb.getModel(modelName)
const db = await getDatabaseRepository<Model>()
const model = await db.find('models', modelName, 'name')

if (!model) {
throw new HttpException(
Expand Down Expand Up @@ -149,8 +149,8 @@ export async function getModelWithWorkflow(

export async function getModels(): Promise<ErrorData<Model[]>> {
try {
const modelsDb = await getModelsDb()
const models = await modelsDb.getModels()
const db = await getDatabaseRepository<Model>()
const models = await db.findAll('models')

return [null, models]
} catch (error) {
Expand Down

0 comments on commit ca6dd50

Please sign in to comment.