feat: initial boilerplate
This commit is contained in:
48
api/libraries/database/accessor.ts
Normal file
48
api/libraries/database/accessor.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Collection, type CollectionOptions, type Db, type Document, type MongoClient } from "mongodb";
|
||||
|
||||
import { container } from "./container.ts";
|
||||
|
||||
export function getDatabaseAccessor<TSchemas extends Record<string, Document>>(
|
||||
database: string,
|
||||
): DatabaseAccessor<TSchemas> {
|
||||
let instance: Db | undefined;
|
||||
return {
|
||||
get db(): Db {
|
||||
if (instance === undefined) {
|
||||
instance = this.client.db(database);
|
||||
}
|
||||
return instance;
|
||||
},
|
||||
get client(): MongoClient {
|
||||
return container.get("client");
|
||||
},
|
||||
collection<TSchema extends keyof TSchemas>(
|
||||
name: TSchema,
|
||||
options?: CollectionOptions,
|
||||
): Collection<TSchemas[TSchema]> {
|
||||
return this.db.collection<TSchemas[TSchema]>(name.toString(), options);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type DatabaseAccessor<TSchemas extends Record<string, Document>> = {
|
||||
/**
|
||||
* Database for given accessor.
|
||||
*/
|
||||
db: Db;
|
||||
|
||||
/**
|
||||
* Lazy loaded mongo client.
|
||||
*/
|
||||
client: MongoClient;
|
||||
|
||||
/**
|
||||
* Returns a reference to a MongoDB Collection. If it does not exist it will be created implicitly.
|
||||
*
|
||||
* Collection namespace validation is performed server-side.
|
||||
*
|
||||
* @param name - Collection name we wish to access.
|
||||
* @param options - Optional settings for the command.
|
||||
*/
|
||||
collection<TSchema extends keyof TSchemas>(name: TSchema, options?: CollectionOptions): Collection<TSchemas[TSchema]>;
|
||||
};
|
||||
10
api/libraries/database/config.ts
Normal file
10
api/libraries/database/config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { getEnvironmentVariable, toNumber } from "~libraries/config/mod.ts";
|
||||
|
||||
export const config = {
|
||||
mongo: {
|
||||
host: getEnvironmentVariable("DB_MONGO_HOST", "localhost"),
|
||||
port: getEnvironmentVariable("DB_MONGO_PORT", toNumber, "27017"),
|
||||
user: getEnvironmentVariable("DB_MONGO_USER", "root"),
|
||||
pass: getEnvironmentVariable("DB_MONGO_PASSWORD", "password"),
|
||||
},
|
||||
};
|
||||
24
api/libraries/database/connection.ts
Normal file
24
api/libraries/database/connection.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { MongoClient } from "mongodb";
|
||||
|
||||
export function getMongoClient(config: MongoConnectionInfo) {
|
||||
return new MongoClient(getConnectionUrl(config));
|
||||
}
|
||||
|
||||
export function getConnectionUrl({ host, port, user, pass }: MongoConnectionInfo): MongoConnectionUrl {
|
||||
return `mongodb://${user}:${pass}@${host}:${port}`;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------
|
||||
| Types
|
||||
|--------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
export type MongoConnectionUrl = `mongodb://${string}:${string}@${string}:${number}`;
|
||||
|
||||
export type MongoConnectionInfo = {
|
||||
host: string;
|
||||
port: number;
|
||||
user: string;
|
||||
pass: string;
|
||||
};
|
||||
6
api/libraries/database/container.ts
Normal file
6
api/libraries/database/container.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Container } from "@valkyr/inverse";
|
||||
import { MongoClient } from "mongodb";
|
||||
|
||||
export const container = new Container<{
|
||||
client: MongoClient;
|
||||
}>("database");
|
||||
3
api/libraries/database/id.ts
Normal file
3
api/libraries/database/id.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import type { CreateIndexesOptions, IndexSpecification } from "mongodb";
|
||||
|
||||
export const idIndex: [IndexSpecification, CreateIndexesOptions] = [{ id: 1 }, { unique: true }];
|
||||
30
api/libraries/database/registrar.ts
Normal file
30
api/libraries/database/registrar.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { CreateIndexesOptions, Db, IndexSpecification } from "mongodb";
|
||||
|
||||
import { getCollectionsSet } from "./utilities.ts";
|
||||
|
||||
/**
|
||||
* Takes a mongo database and registers the event store collections and
|
||||
* indexes defined internally.
|
||||
*
|
||||
* @param db - Mongo database to register event store collections against.
|
||||
* @param registrars - List of registrars to register with the database.
|
||||
* @param logger - Logger method to print internal logs.
|
||||
*/
|
||||
export async function register(db: Db, registrars: Registrar[], logger?: (...args: any[]) => any) {
|
||||
const list = await getCollectionsSet(db);
|
||||
for (const { name, indexes } of registrars) {
|
||||
if (list.has(name) === false) {
|
||||
await db.createCollection(name);
|
||||
}
|
||||
for (const [indexSpec, options] of indexes) {
|
||||
await db.collection(name).createIndex(indexSpec, options);
|
||||
logger?.("Mongo Event Store > Collection '%s' is indexed [%O] with options %O", name, indexSpec, options ?? {});
|
||||
}
|
||||
logger?.("Mongo Event Store > Collection '%s' is registered", name);
|
||||
}
|
||||
}
|
||||
|
||||
export type Registrar = {
|
||||
name: string;
|
||||
indexes: [IndexSpecification, CreateIndexesOptions?][];
|
||||
};
|
||||
5
api/libraries/database/tasks/bootstrap.ts
Normal file
5
api/libraries/database/tasks/bootstrap.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { config } from "../config.ts";
|
||||
import { getMongoClient } from "../connection.ts";
|
||||
import { container } from "../container.ts";
|
||||
|
||||
container.set("client", getMongoClient(config.mongo));
|
||||
37
api/libraries/database/utilities.ts
Normal file
37
api/libraries/database/utilities.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { Db } from "mongodb";
|
||||
import z, { ZodType } from "zod";
|
||||
|
||||
/**
|
||||
* Get a Set of collections that exists on a given mongo database instance.
|
||||
*
|
||||
* @param db - Mongo database to fetch collection list for.
|
||||
*/
|
||||
export async function getCollectionsSet(db: Db) {
|
||||
return db
|
||||
.listCollections()
|
||||
.toArray()
|
||||
.then((collections) => new Set(collections.map((c) => c.name)));
|
||||
}
|
||||
|
||||
export function toParsedDocuments<TSchema extends ZodType>(
|
||||
schema: TSchema,
|
||||
): (documents: unknown[]) => Promise<z.infer<TSchema>[]> {
|
||||
return async function (documents: unknown[]) {
|
||||
const parsed = [];
|
||||
for (const document of documents) {
|
||||
parsed.push(await schema.parseAsync(document));
|
||||
}
|
||||
return parsed;
|
||||
};
|
||||
}
|
||||
|
||||
export function toParsedDocument<TSchema extends ZodType>(
|
||||
schema: TSchema,
|
||||
): (document?: unknown) => Promise<z.infer<TSchema> | undefined> {
|
||||
return async function (document: unknown) {
|
||||
if (document === undefined || document === null) {
|
||||
return undefined;
|
||||
}
|
||||
return schema.parseAsync(document);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user