@elysiumjs/mnemosyne 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  $ bun run clean && bun run build:js && bun run build:ts
2
2
  $ rimraf dist
3
- $ bun build ./src/index.ts --outdir ./dist --target bun --packages external --root ./src --minify
4
- Bundled 7 modules in 23ms
3
+ $ bun build ./src/index.ts --outdir ./dist --target bun --packages external --root ./src --minify-syntax --minify-whitespace
4
+ Bundled 8 modules in 21ms
5
5
 
6
- index.js 7.47 KB (entry point)
6
+ index.js 11.16 KB (entry point)
7
7
 
8
8
  $ tsc
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # @elysiumjs/mnemosyne
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`3625883`](https://github.com/na2axl/elysiumjs/commit/3625883dba92f1596e7444ee52f9399d8e9b049f) Thanks [@na2axl](https://github.com/na2axl)! - Drop the `reflect-metadata` dependency. Decorator metadata now lives in a small store in `@elysiumjs/core`, exported as `Metadata` (`define`, `get`, `getOwn`, `has`). Use it instead of `Reflect.defineMetadata` and `Reflect.getMetadata` to read or write Elysium metadata.
8
+
9
+ Apps no longer need `import 'reflect-metadata'` in their entrypoint, and generated projects leave it out. Keep `emitDecoratorMetadata` enabled: `@Service.inject()` and `@Command.arg()` still infer types from it. An app that loads `reflect-metadata` for another library keeps working; Elysium also reads the types stored there.
10
+
11
+ - [`07236b3`](https://github.com/na2axl/elysiumjs/commit/07236b386c2506590d74e8753ce847c1e184b3ee) Thanks [@na2axl](https://github.com/na2axl)! - Introduce the module system. Modules are now the only way to register anything in an Elysium.js app. This is a breaking change with no compatibility layer.
12
+
13
+ Breaking changes:
14
+
15
+ - `@Module.register()` takes `imports`, `services`, `exports`, `controllers`, `commands`, `jobs`, `prefix` and `env`. Services are private to their module unless exported (or global). The `beforeRegister` and `afterRegister` hooks are replaced by `onInit`, `onServe` and `onDestroy`.
16
+ - Configurable modules are bound with `with()`, for example `HermesModule.with({ level: 'debug' })`. `with()` accepts an object or a function.
17
+ - Decorators only record metadata. Importing a file no longer registers commands, services or event wiring, and the side-effect imports of the packages are removed. List commands, jobs and services in the module that owns them.
18
+ - `@Application.register()` no longer takes `database`, `redis`, `wamp` or `'elysium:hermes'`/`'elysium:cerberus'` keys. Use `DrizzleModule`, `RedisModule`, `WampModule`, `HermesModule` and `CerberusModule`. Package plugins are replaced by modules: add `SentryModule` instead of the `plugin()` function. `plugins` still accepts plain Elysia plugin factories.
19
+ - Constructing an `Application` no longer runs it. Use `await App.start()`; `Application.testing()` boots a module graph for tests. The `styx` binary loads `src/app.ts` (or `index.ts`) and starts the exported application, so a project entry no longer needs to import commands.
20
+ - The job system of core (`Job`, `Worker`, `WorkerPool`, `commandWork`) is removed. `@elysiumjs/heracles` is the only job system: extend its `Job`, list jobs in a module's `jobs`, declare queues with `HeraclesModule.with({ queues })`, and dispatch with `Heracles.dispatch()`.
21
+ - `@elysiumjs/styx` ships `StyxModule`, active in development only. The `migration:generate` and `migration:run` commands move to `DrizzleModule` and are available in production.
22
+ - `@elysiumjs/mnemosyne` ships `MnemosyneModule` for its caches (`redis: false` or `memory: false` opts out). `DrizzleModule` imports it.
23
+ - New modules: `HephaestusModule`, `ArgusModule` (`serviceName` is required), `SentryModule` (needs a `dsn` or `SENTRY_DSN`) and `CerberusModule` (`getSubject` is required).
24
+ - Module and service code throws typed errors (for example `InvalidModuleConfigError`) instead of exiting the process. `Application.run()` prints them and exits; command `run()` methods may still exit, and package plugin factories are unchanged.
25
+ - `Application.register` merges subclass props over the parent's (own props, then own metadata, then parent, then defaults), and concatenates `modules` parent first. Previously defaults and parent values won, so `openapi` could not be enabled.
26
+ - `Heracles.dispatch` starts a stopped queue on first use, so producers no longer start queues. `Queue.start()` is idempotent.
27
+ - `create-elysium` generates apps that use modules, and adds Sentry and Hephaestus to the `modules` list.
28
+ - `@Event.on`/`@Event.once` method listeners on classes in a loaded module's `services` or `controllers` are wired by the kernel and run in their module's context. Jobs, commands and middlewares are not wired, and an instance listener on a class that is not a service throws `EventListenerNotSupportedError`. Listeners on classes not listed in a loaded module no longer fire.
29
+ - New typed errors: `CircularDependencyError` (services that depend on each other, in sync and async resolution), `DuplicateModuleNameError` (two different module classes sharing a name; module scopes and config tokens are name based) and `EventListenerNotSupportedError`.
30
+ - Lazy boot contract: a CLI command initialises only its own module and that module's imports. Sync helpers such as `Database.getConnection()` work when the owning module's `onInit` is synchronous; otherwise they throw `ServiceNotReadyError` naming the module. Use `await Service.resolve()` or set `static boot = true` on the command.
31
+
32
+ ### Patch Changes
33
+
34
+ - Updated dependencies [[`8ac91ab`](https://github.com/na2axl/elysiumjs/commit/8ac91ab676b1119b4eecf0ddb3fd0a3e84017cac), [`3625883`](https://github.com/na2axl/elysiumjs/commit/3625883dba92f1596e7444ee52f9399d8e9b049f), [`07236b3`](https://github.com/na2axl/elysiumjs/commit/07236b386c2506590d74e8753ce847c1e184b3ee)]:
35
+ - @elysiumjs/core@0.8.0
36
+
3
37
  ## 0.1.0
4
38
 
5
39
  ### Minor Changes
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @elysiumjs/mnemosyne
2
2
 
3
- Database and caching for Elysium.js applications.
3
+ Database abstractions and caching for Elysium.js applications.
4
4
 
5
5
  ## Installation
6
6
 
@@ -10,24 +10,27 @@ bun add @elysiumjs/mnemosyne
10
10
 
11
11
  ## Usage
12
12
 
13
+ `MnemosyneModule` provides the caches (`Cache.redis` and `Cache.memory`). Database driver modules such as `DrizzleModule` from `@elysiumjs/mnemosyne-drizzle` import it for you, so you only add it yourself when you want to configure the caches or use them without a database:
14
+
13
15
  ```ts
14
- import { Database, Model, Repository } from '@elysiumjs/mnemosyne';
15
- import { Service, ServiceScope } from '@elysiumjs/core';
16
- import { uuid, varchar } from 'drizzle-orm/pg-core';
17
-
18
- // Define a model
19
- class UserModel extends Model('users', {
20
- id: uuid().primaryKey().defaultRandom(),
21
- name: varchar({ length: 255 }).notNull(),
22
- }) {
23
- public static readonly supportTenancy = true;
24
- }
25
-
26
- // Create a repository
27
- @Service.register({ name: 'UserRepository', scope: ServiceScope.SINGLETON })
28
- class UserRepository extends Repository(UserModel) {}
16
+ import { Application, RedisModule } from '@elysiumjs/core';
17
+ import { MnemosyneModule } from '@elysiumjs/mnemosyne';
18
+
19
+ @Application.register({
20
+ modules: [
21
+ RedisModule.with({
22
+ default: 'cache',
23
+ connections: { cache: { url: process.env.REDIS_URL! } }
24
+ }),
25
+ // Opt out of a cache with `redis: false` or `memory: false`.
26
+ MnemosyneModule.with({ cache: { memory: { ttl: 60_000, lruSize: 5000 } } })
27
+ ]
28
+ })
29
+ export class App extends Application {}
29
30
  ```
30
31
 
32
+ Models and repositories are built with a driver package. See [`@elysiumjs/mnemosyne-drizzle`](../mnemosyne-drizzle/README.md).
33
+
31
34
  ## License
32
35
 
33
36
  This project is licensed under the Apache License, Version 2.0. See the [LICENSE](../../LICENSE) file for more info.
package/dist/cache.d.ts CHANGED
@@ -1,3 +1,8 @@
1
+ import type { KeyvStoreAdapter } from 'keyv';
2
+ /** Name of the global service backing `Cache.redis`. */
3
+ export declare const REDIS_CACHE_SERVICE = "mnemosyne.cache.redis";
4
+ /** Name of the global service backing `Cache.memory`. */
5
+ export declare const MEMORY_CACHE_SERVICE = "mnemosyne.cache.memory";
1
6
  /**
2
7
  * Interface for a cache storage.
3
8
  * @author Axel Nana <axel.nana@workbud.com>
@@ -76,8 +81,18 @@ export interface CacheInterface {
76
81
  */
77
82
  tags(...tags: string[]): Omit<CacheInterface, 'tags'>;
78
83
  }
84
+ /**
85
+ * Makes a cache interface wrapping the given store.
86
+ * @author Axel Nana <axel.nana@workburd.com>
87
+ * @param store The cache storage to use in the interface.
88
+ * @returns A new `CacheInterface`.
89
+ */
90
+ export declare const makeCacheInterface: (store: KeyvStoreAdapter) => CacheInterface;
91
+ /**
92
+ * Static access to the caches provided by the `MnemosyneModule`.
93
+ * @author Axel Nana <axel.nana@workbud.com>
94
+ */
79
95
  export declare abstract class Cache {
80
- #private;
81
96
  /**
82
97
  * A weak cache storage for simple non-intrusive caching of data.
83
98
  * @author Axel Nana <axel.nana@workbud.com>
@@ -103,11 +118,13 @@ export declare abstract class Cache {
103
118
  /**
104
119
  * Redis-based cache storage.
105
120
  * @author Axel Nana <axel.nana@workbud.com>
121
+ * @throws {ServiceNotFoundError} If the `MnemosyneModule` is not loaded or has no redis cache.
106
122
  */
107
123
  static get redis(): CacheInterface;
108
124
  /**
109
125
  * Memory-based cache storage.
110
126
  * @author Axel Nana <axel.nana@workbud.com>
127
+ * @throws {ServiceNotFoundError} If the `MnemosyneModule` is not loaded or has no memory cache.
111
128
  */
112
129
  static get memory(): CacheInterface;
113
130
  }
@@ -7,12 +7,12 @@ import type { DatabaseCacheConfig, DatabaseDriver } from './interfaces';
7
7
  * @param config The database cache configuration.
8
8
  * @returns A cache interface tagged with 'database'.
9
9
  */
10
- export declare const createDatabaseCacheStorage: (config: DatabaseCacheConfig) => Omit<CacheInterface, "tags">;
10
+ export declare const createDatabaseCacheStorage: (config: DatabaseCacheConfig) => Omit<CacheInterface, 'tags'>;
11
11
  /**
12
12
  * Abstract database manager.
13
13
  *
14
- * Manages named database connections using a driver. Connections are
15
- * registered in the service container and can be retrieved by name.
14
+ * Retrieves named database connections using a driver. Connections are registered as
15
+ * global services `db.connection.{name}` by the driver modules.
16
16
  *
17
17
  * @author Axel Nana <axel.nana@workbud.com>
18
18
  * @template TConnection The connection type managed by the driver.
@@ -24,34 +24,25 @@ export declare abstract class AbstractDatabase<TConnection, TConnectionConfig> {
24
24
  */
25
25
  protected abstract driver: DatabaseDriver<TConnection, TConnectionConfig>;
26
26
  /**
27
- * Creates a service name for a database connection.
27
+ * Creates a service name for a database connection. Driver modules register their
28
+ * connections (and the `default` alias) under this name.
29
+ * @author Axel Nana <axel.nana@workbud.com>
28
30
  * @param name The name of the connection.
29
31
  * @returns A service name for the connection.
30
32
  */
31
- private getConnectionName;
33
+ static getConnectionName(name: string): string;
32
34
  /**
33
35
  * Retrieves the connection with the given name.
34
36
  *
35
37
  * For the default connection, checks the async context for a
36
38
  * transaction handle (`db:tx`) before falling back to the service container.
39
+ * @throws {ServiceNotFoundError} If no connection with that name is registered.
37
40
  *
38
41
  * @author Axel Nana <axel.nana@workbud.com>
39
42
  * @param name The connection name.
40
43
  * @returns The connection with the given name.
41
44
  */
42
45
  getConnection(name?: string): TConnection;
43
- /**
44
- * Creates and registers a new connection.
45
- *
46
- * This will make the registered connection available for dependency injection with
47
- * the key `db.connection.{name}`, where `{name}` is replaced with the given name.
48
- *
49
- * @author Axel Nana <axel.nana@workbud.com>
50
- * @param name The connection name.
51
- * @param config The connection configuration.
52
- * @returns The newly created and registered connection.
53
- */
54
- registerConnection(name: string, config: TConnectionConfig): TConnection;
55
46
  /**
56
47
  * Checks if a connection with the given name exists.
57
48
  * @author Axel Nana <axel.nana@workbud.com>
@@ -65,13 +56,6 @@ export declare abstract class AbstractDatabase<TConnection, TConnectionConfig> {
65
56
  * @returns The default connection.
66
57
  */
67
58
  getDefaultConnection(): TConnection;
68
- /**
69
- * Sets the default connection.
70
- * @author Axel Nana <axel.nana@workbud.com>
71
- * @param name The name of the connection to set as default.
72
- * @returns The default connection.
73
- */
74
- setDefaultConnection(name: string): TConnection;
75
59
  /**
76
60
  * Returns the raw underlying client for the given connection.
77
61
  * @author Axel Nana <axel.nana@workbud.com>
package/dist/index.d.ts CHANGED
@@ -1,17 +1,9 @@
1
- import type { TenancyConfig } from './tenancy';
2
1
  export type { ColumnMetadata, DatabaseCacheConfig, DatabaseCacheStrategy, DatabaseDriver, IdType, ModelAdapter, ModelClass, RepositoryClass, RepositoryInterface, TenancyStrategy } from './interfaces';
3
- export { AbstractDatabase, createDatabaseCacheStorage } from './database';
4
- export { createSchemaFromModel, AbstractModel } from './model';
5
- export { AbstractRepository } from './repository';
6
- export { getConfig as getTenancyConfig, getCurrentTenant, withTenant, registerTenancyStrategy, getTenancyStrategy, TenantMiddleware, SimpleTenantMiddleware, StrictTenantMiddleware } from './tenancy';
2
+ export * from './database';
3
+ export * from './model';
4
+ export * from './repository';
5
+ export * from './tenancy';
7
6
  export type { TenancyConfig, TenancyMode, ModelTenancyConfig } from './tenancy';
8
- export { Cache } from './cache';
9
- export type { CacheInterface } from './cache';
10
- export { KeyvRedis } from './keyv-redis';
11
- export interface MnemosyneConfig<TConnectionConfig = unknown> {
12
- database?: {
13
- default: string;
14
- connections: Record<string, TConnectionConfig>;
15
- };
16
- tenancy?: TenancyConfig;
17
- }
7
+ export { Cache, type CacheInterface, MEMORY_CACHE_SERVICE, REDIS_CACHE_SERVICE } from './cache';
8
+ export * from './mnemosyne.module';
9
+ export * from './keyv-redis';
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // @bun
2
- import{Application as P,Service as G}from"@elysiumjs/core";import{createCache as E}from"cache-manager";import{KeyvCacheableMemory as N}from"cacheable";import{Keyv as F}from"keyv";import{alphabetical as A}from"radash";import{EventEmitter as K}from"events";import{Event as C,Redis as w}from"@elysiumjs/core";class J extends K{#D;#g;#L="::";#I=1000;#R=!0;#O=!1;#W;constructor(g){super();C.once("elysium:app:launched",()=>{this.#D=w.getConnection(g.connection)}),this.#W=g.connection,this.#g=g.namespace,this.#L=g.keyPrefixSeparator??"::",this.#I=g.clearBatchSize??1000,this.#R=g.useUnlink??!0,this.#O=g.noNamespaceAffectsAll??!1,this.on("error",(L)=>{C.emit("elysium:error",L)})}get client(){return this.#D}get opts(){return{namespace:this.#g,keyPrefixSeparator:this.#L,clearBatchSize:this.#I,noNamespaceAffectsAll:this.#O,dialect:"redis",url:this.#W}}get namespace(){return this.#g}set namespace(g){this.#g=g}async get(g){g=this.createKeyPrefix(g,this.#g);let L=await this.client.get(g);return L===null?void 0:L}async set(g,L,D){if(g=this.createKeyPrefix(g,this.#g),D)await this.client.set(g,L,"PX",D);else await this.client.set(g,L)}async setMany(g){let L=[];for(let{key:D,value:O,ttl:R}of g){let I=this.createKeyPrefix(D,this.#g);if(R)L.push(this.client.set(I,O,"PX",R));else L.push(this.client.set(I,O))}await Promise.all(L)}async delete(g){return g=this.createKeyPrefix(g,this.#g),await(this.#R?this.client.send("UNLINK",[g]):this.client.del(g))>0}async clear(){try{if(!this.#g&&this.#O){await this.client.send("FLUSHDB",[]);return}let g="0",L=this.#I,D=this.#g?`${this.#g}${this.#L}*`:"*",O=[];do{let R=await this.client.send("SCAN",[g,"MATCH",D,"COUNT",L.toString(),"TYPE","string"]),[I,Y]=R;if(g=I.toString(),Y.length===0)continue;if(!this.#g)Y=Y.filter((q)=>!q.includes(this.#L));O.push(Y.map((q)=>this.client.del(q)))}while(g!=="0");await Promise.all(O)}catch(g){this.emit("error",g)}}async has(g){return g=this.createKeyPrefix(g,this.#g),this.client.exists(g)}async hasMany(g){let L=[];for(let D of g){let O=this.createKeyPrefix(D,this.#g);L.push(this.client.exists(O))}return Promise.all(L)}async getMany(g){if(g.length===0)return[];return g=g.map((D)=>this.createKeyPrefix(D,this.#g)),await this.client.mget(...g)}async disconnect(){this.client.close()}async deleteMany(g){let L=!1,D=[];for(let R of g){let I=this.createKeyPrefix(R,this.#g);if(this.#R)D.push(this.client.send("UNLINK",[I]));else D.push(this.client.del(I))}let O=await Promise.all(D);for(let R of O)if(typeof R==="number"&&R>0)L=!0;return L}async*iterator(g){let L=g?`${g}${this.#L}*`:"*",D="0";do{let O=await this.client.send("SCAN",[D,"MATCH",L,"TYPE","string"]),[R,I]=O;if(D=R.toString(),!g&&!this.#O)I=I.filter((Y)=>!Y.includes(this.#L));if(I.length>0){let Y=await this.client.mget(...I);for(let q of I.keys()){let j=this.getKeyWithoutPrefix(I[q],g),f=Y[q];yield[j,f]}}}while(D!=="0")}createKeyPrefix(g,L){return L?`${L}${this.#L}${g}`:g}getKeyWithoutPrefix(g,L){return L?g.replace(`${L}${this.#L}`,""):g}}var U=(g)=>{let L=E({stores:[new F({store:g,namespace:"cache"})]}),D=new Map;return{...L,tags(...O){let R=`cache__${A(O,(Y)=>Y).join("__")}`,I=D.get(R);if(!I)I=E({stores:[new F({store:g,namespace:R})]}),D.set(R,I);return I}}};class V{static#D;static#g;static weak=Object.seal({map:new WeakMap,get(g){if(!this.map.has(g))this.map.set(g,{});return this.map.get(g)},del(g){this.map.delete(g)}});static get redis(){if(!V.#D)V.#D=U(new J({connection:"cache"}));return V.#D}static get memory(){if(!V.#g)V.#g=U(new N({ttl:60000,lruSize:5000}));return V.#g}}var S=(g)=>{return V[g.storage??"redis"].tags("database")};class B{getConnectionName(g){return`db.connection.${g}`}getConnection(g="default"){if(g==="default"){let D=P.context.getStore()?.get("db:tx");if(D)return D}let L=this.getConnectionName(g);if(!G.exists(L))console.error(`No connection with name ${g} found. Please make sure to register the connection before using it.`),process.exit(1);return G.get(L)}registerConnection(g,L){let D=this.getConnectionName(g);if(G.exists(D))console.error(`A connection with the name ${g} has already been registered.`),process.exit(1);let O=this.driver.createConnection(L);return G.instance(D,O)}connectionExists(g){return G.exists(this.getConnectionName(g))}getDefaultConnection(){return this.getConnection("default")}setDefaultConnection(g){let L=this.getConnectionName("default");return G.remove(L),G.instance(L,this.getConnection(g))}getRawClient(g="default"){return this.driver.getRawClient(this.getConnection(g))}}import{Application as M}from"@elysiumjs/core";import{t as W}from"elysia";import{Application as H,Middleware as b}from"@elysiumjs/core";var _=null,x=()=>{let g=H.instance;if(!g)return{mode:"schema"};return g.getConfig("elysium:mnemosyne")?.tenancy??{mode:"schema"}},v=(g)=>{_=g},Z=()=>{return _},$=()=>{if(!H.instance||!H.context.getStore)return null;return H.context.getStore()?.get("tenant")??null},d=(g,L)=>{let D=H.context.getStore(),O=new Map(D);return O.set("tenant",g),H.context.run(O,L)};class Q extends b{header="x-tenant-id";allowPublic=!0;connectionName="default";async validateTenant(g){return!0}async onTenantResolved(g,L){}onTenantError(g,L){if(g==="missing")throw Error("Tenant ID required");else throw Error("Invalid tenant")}async onRequest(g){let L=g.request.headers.get(this.header);if(!L||L==="public"){if(this.allowPublic){g.tenant="public";return}return this.onTenantError("missing",g)}if(!await this.validateTenant(L))return this.onTenantError("invalid",g);g.tenant=L,await this.onTenantResolved(L,g)}}class T extends Q{}class z extends Q{allowPublic=!1}var X=(g,{mode:L="select"}={})=>{if(g===void 0||g.length===0)return W.Object({});let D={};for(let O of g){if(["create","update"].includes(L)&&O.isPrimaryKey)continue;if(D[O.name]=h(O),O.nullable)D[O.name]=W.Nullable(D[O.name]);if(L==="update"||O.hasDefault||O.nullable)D[O.name]=W.Optional(D[O.name])}return W.Object(D)},h=(g)=>{switch(g.dataType){case"string":return W.String();case"uuid":return W.String({format:"uuid"});case"number":return W.Number();case"boolean":return W.Boolean();case"array":return W.Array(W.Any());case"json":return W.Object({});case"date":return W.Transform(W.String({format:"date"})).Decode((L)=>new Date(L)).Encode((L)=>L.toISOString());case"datetime":return W.Transform(W.String({format:"date-time"})).Decode((L)=>new Date(L)).Encode((L)=>L.toISOString());case"bigint":return W.BigInt();case"buffer":return W.File();default:return W.Never()}},i=(g,L,D,O)=>{let R=D.getColumns(O);class I{static $inferSelect=void 0;static $inferInsert=void 0;static $inferUpdate=void 0;static get table(){if(!M.instance||!M.context.getStore)return O;if(this.supportTenancy){let Y=$()??"public";if(Y!=="public"){let q=Z();if(q)return q.resolveTable(O,Y);return D.createTenantTable(O,Y)}}return O}static tableName=g;static columns=L;static insertSchema=X(R,{mode:"create"});static updateSchema=X(R,{mode:"update"});static selectSchema=X(R,{mode:"select"});static supportTenancy=!1}return I};var u=(g,L)=>{class D{static Model=g;static connection="default";get db(){return L.getConnection(this.constructor.connection)}}return D};export{d as withTenant,v as registerTenancyStrategy,Z as getTenancyStrategy,x as getTenancyConfig,$ as getCurrentTenant,X as createSchemaFromModel,S as createDatabaseCacheStorage,Q as TenantMiddleware,z as StrictTenantMiddleware,T as SimpleTenantMiddleware,J as KeyvRedis,V as Cache,u as AbstractRepository,i as AbstractModel,B as AbstractDatabase};
2
+ var __legacyDecorateClassTS=function(decorators,target,key,desc){var c=arguments.length,r=c<3?target:desc===null?desc=Object.getOwnPropertyDescriptor(target,key):desc,d;if(typeof Reflect==="object"&&typeof Reflect.decorate==="function")r=Reflect.decorate(decorators,target,key,desc);else for(var i=decorators.length-1;i>=0;i--)if(d=decorators[i])r=(c<3?d(r):c>3?d(target,key,r):d(target,key))||r;return c>3&&r&&Object.defineProperty(target,key,r),r};import{Application,Service as Service2,ServiceNotFoundError as ServiceNotFoundError2}from"@elysiumjs/core";import{Service,ServiceNotFoundError}from"@elysiumjs/core";import{createCache}from"cache-manager";import{Keyv}from"keyv";import{alphabetical}from"radash";var REDIS_CACHE_SERVICE="mnemosyne.cache.redis",MEMORY_CACHE_SERVICE="mnemosyne.cache.memory",makeCacheInterface=(store)=>{let base=createCache({stores:[new Keyv({store,namespace:"cache"})]}),tagsCache=new Map;return{...base,tags(...tags){let namespace=`cache__${alphabetical(tags,(item)=>item).join("__")}`,cache=tagsCache.get(namespace);if(!cache)cache=createCache({stores:[new Keyv({store,namespace})]}),tagsCache.set(namespace,cache);return cache}}},getCache=(name)=>{let cache=Service.get(name);if(!cache)throw new ServiceNotFoundError(name);return cache};class Cache{static weak=Object.seal({map:new WeakMap,get(ctx){if(!this.map.has(ctx))this.map.set(ctx,{});return this.map.get(ctx)},del(ctx){this.map.delete(ctx)}});static get redis(){return getCache(REDIS_CACHE_SERVICE)}static get memory(){return getCache(MEMORY_CACHE_SERVICE)}}var createDatabaseCacheStorage=(config)=>Cache[config.storage??"redis"].tags("database");class AbstractDatabase{static getConnectionName(name){return`db.connection.${name}`}getConnection(name="default"){if(name==="default"){let tx=Application.context.getStore()?.get("db:tx");if(tx)return tx}let serviceName=AbstractDatabase.getConnectionName(name),connection=Service2.get(serviceName);if(connection===null)throw new ServiceNotFoundError2(serviceName);return connection}connectionExists(name){return Service2.exists(AbstractDatabase.getConnectionName(name))}getDefaultConnection(){return this.getConnection("default")}getRawClient(name="default"){return this.driver.getRawClient(this.getConnection(name))}}import{Application as Application3}from"@elysiumjs/core";import{t}from"elysia";import{Application as Application2,Middleware,Service as Service3}from"@elysiumjs/core";var TENANCY_CONFIG_SERVICE="mnemosyne.tenancy.config",TENANCY_STRATEGY_SERVICE="mnemosyne.tenancy.strategy",getTenancyConfig=()=>Service3.get(TENANCY_CONFIG_SERVICE)??{mode:"schema"},getTenancyStrategy=()=>Service3.get(TENANCY_STRATEGY_SERVICE),getCurrentTenant=()=>{if(!Application2.instance||!Application2.context.getStore)return null;return Application2.context.getStore()?.get("tenant")??null},withTenant=(tenant,callback)=>{let currentStore=Application2.context.getStore(),newStore=new Map(currentStore);return newStore.set("tenant",tenant),Application2.context.run(newStore,callback)};class TenantMiddleware extends Middleware{header="x-tenant-id";allowPublic=!0;connectionName="default";async validateTenant(_tenant){return!0}async onTenantResolved(_tenant,_ctx){}onTenantError(reason,_ctx){if(reason==="missing")throw Error("Tenant ID required");else throw Error("Invalid tenant")}async onRequest(ctx){let tenantId=ctx.request.headers.get(this.header);if(!tenantId||tenantId==="public"){if(this.allowPublic){ctx.tenant="public";return}return this.onTenantError("missing",ctx)}if(!await this.validateTenant(tenantId))return this.onTenantError("invalid",ctx);ctx.tenant=tenantId,await this.onTenantResolved(tenantId,ctx)}}class SimpleTenantMiddleware extends TenantMiddleware{}class StrictTenantMiddleware extends TenantMiddleware{allowPublic=!1}var createSchemaFromModel=(columns,{mode="select"}={})=>{if(columns===void 0||columns.length===0)return t.Object({});let properties={};for(let column of columns){if(["create","update"].includes(mode)&&column.isPrimaryKey)continue;if(properties[column.name]=parseColumnType(column),column.nullable)properties[column.name]=t.Nullable(properties[column.name]);if(mode==="update"||column.hasDefault||column.nullable)properties[column.name]=t.Optional(properties[column.name])}return t.Object(properties)},parseColumnType=(column)=>{switch(column.dataType){case"string":return t.String();case"uuid":return t.String({format:"uuid"});case"number":return t.Number();case"boolean":return t.Boolean();case"array":return t.Array(t.Any());case"json":return t.Object({});case"date":return t.Transform(t.String({format:"date"})).Decode((date)=>new Date(date)).Encode((date)=>date.toISOString());case"datetime":return t.Transform(t.String({format:"date-time"})).Decode((date)=>new Date(date)).Encode((date)=>date.toISOString());case"bigint":return t.BigInt();case"buffer":return t.File();default:return t.Never()}},AbstractModel=(tableName,columns,adapter,baseTable)=>{let columnMetadata=adapter.getColumns(baseTable);class M{static $inferSelect=void 0;static $inferInsert=void 0;static $inferUpdate=void 0;static get table(){if(!Application3.instance||!Application3.context.getStore)return baseTable;if(this.supportTenancy){let tenant=getCurrentTenant()??"public";if(tenant!=="public"){let strategy=getTenancyStrategy();if(strategy)return strategy.resolveTable(baseTable,tenant);return adapter.createTenantTable(baseTable,tenant)}}return baseTable}static tableName=tableName;static columns=columns;static insertSchema=createSchemaFromModel(columnMetadata,{mode:"create"});static updateSchema=createSchemaFromModel(columnMetadata,{mode:"update"});static selectSchema=createSchemaFromModel(columnMetadata,{mode:"select"});static supportTenancy=!1}return M};var AbstractRepository=(model,database)=>{class R{static Model=model;static connection="default";get db(){return database.getConnection(this.constructor.connection)}}return R};import{Module}from"@elysiumjs/core";import{KeyvCacheableMemory}from"cacheable";import{EventEmitter}from"events";import{Event,Redis,Service as Service4}from"@elysiumjs/core";class KeyvRedis extends EventEmitter{#client;#namespace;#keyPrefixSeparator="::";#clearBatchSize=1000;#useUnlink=!0;#noNamespaceAffectsAll=!1;#connection;constructor(props){super();this.#connection=props.connection,this.#namespace=props.namespace,this.#keyPrefixSeparator=props.keyPrefixSeparator??"::",this.#clearBatchSize=props.clearBatchSize??1000,this.#useUnlink=props.useUnlink??!0,this.#noNamespaceAffectsAll=props.noNamespaceAffectsAll??!1,this.on("error",(error)=>{Event.emit("elysium:error",error)})}async getClient(){return this.#client??=await Service4.resolve(Redis.getConnectionName(this.#connection)),this.#client}get opts(){return{namespace:this.#namespace,keyPrefixSeparator:this.#keyPrefixSeparator,clearBatchSize:this.#clearBatchSize,noNamespaceAffectsAll:this.#noNamespaceAffectsAll,dialect:"redis",url:this.#connection}}get namespace(){return this.#namespace}set namespace(value){this.#namespace=value}async get(key){let client=await this.getClient();key=this.createKeyPrefix(key,this.#namespace);let value=await client.get(key);return value===null?void 0:value}async set(key,value,ttl){let client=await this.getClient();if(key=this.createKeyPrefix(key,this.#namespace),ttl)await client.set(key,value,"PX",ttl);else await client.set(key,value)}async setMany(values){let client=await this.getClient(),multi=[];for(let{key,value,ttl}of values){let prefixedKey=this.createKeyPrefix(key,this.#namespace);if(ttl)multi.push(client.set(prefixedKey,value,"PX",ttl));else multi.push(client.set(prefixedKey,value))}await Promise.all(multi)}async delete(key){let client=await this.getClient();return key=this.createKeyPrefix(key,this.#namespace),await(this.#useUnlink?client.send("UNLINK",[key]):client.del(key))>0}async clear(){try{let client=await this.getClient();if(!this.#namespace&&this.#noNamespaceAffectsAll){await client.send("FLUSHDB",[]);return}let cursor="0",batchSize=this.#clearBatchSize,match=this.#namespace?`${this.#namespace}${this.#keyPrefixSeparator}*`:"*",deletePromises=[];do{let result=await client.send("SCAN",[cursor,"MATCH",match,"COUNT",batchSize.toString(),"TYPE","string"]),[newCursor,keys]=result;if(cursor=newCursor.toString(),keys.length===0)continue;if(!this.#namespace)keys=keys.filter((key)=>!key.includes(this.#keyPrefixSeparator));deletePromises.push(keys.map((key)=>client.del(key)))}while(cursor!=="0");await Promise.all(deletePromises)}catch(error){this.emit("error",error)}}async has(key){let client=await this.getClient();return key=this.createKeyPrefix(key,this.#namespace),client.exists(key)}async hasMany(keys){let client=await this.getClient(),multi=[];for(let key of keys){let prefixedKey=this.createKeyPrefix(key,this.#namespace);multi.push(client.exists(prefixedKey))}return Promise.all(multi)}async getMany(keys){let client=await this.getClient();if(keys.length===0)return[];return keys=keys.map((key)=>this.createKeyPrefix(key,this.#namespace)),await client.mget(...keys)}async disconnect(){this.#client=void 0}async deleteMany(keys){let client=await this.getClient(),result=!1,multi=[];for(let key of keys){let prefixedKey=this.createKeyPrefix(key,this.#namespace);if(this.#useUnlink)multi.push(client.send("UNLINK",[prefixedKey]));else multi.push(client.del(prefixedKey))}let results=await Promise.all(multi);for(let deleted of results)if(typeof deleted==="number"&&deleted>0)result=!0;return result}async*iterator(namespace){let client=await this.getClient(),match=namespace?`${namespace}${this.#keyPrefixSeparator}*`:"*",cursor="0";do{let result=await client.send("SCAN",[cursor,"MATCH",match,"TYPE","string"]),[newCursor,keys]=result;if(cursor=newCursor.toString(),!namespace&&!this.#noNamespaceAffectsAll)keys=keys.filter((key)=>!key.includes(this.#keyPrefixSeparator));if(keys.length>0){let values=await client.mget(...keys);for(let i of keys.keys()){let key=this.getKeyWithoutPrefix(keys[i],namespace),value=values[i];yield[key,value]}}}while(cursor!=="0")}createKeyPrefix(key,namespace){return namespace?`${namespace}${this.#keyPrefixSeparator}${key}`:key}getKeyWithoutPrefix(key,namespace){return namespace?key.replace(`${namespace}${this.#keyPrefixSeparator}`,""):key}}class MnemosyneModule extends Module.configurable({defaults:{cache:{redis:{connection:"cache"},memory:{ttl:60000,lruSize:5000}}}}){}MnemosyneModule=__legacyDecorateClassTS([Module.register({services:(config)=>{let services=[],{redis,memory}=config.cache;if(redis)services.push({name:REDIS_CACHE_SERVICE,global:!0,factory:()=>makeCacheInterface(new KeyvRedis({connection:redis.connection}))});if(memory)services.push({name:MEMORY_CACHE_SERVICE,global:!0,factory:()=>makeCacheInterface(new KeyvCacheableMemory({ttl:memory.ttl,lruSize:memory.lruSize}))});return services}})],MnemosyneModule);export{AbstractDatabase,AbstractModel,AbstractRepository,Cache,KeyvRedis,MEMORY_CACHE_SERVICE,MnemosyneModule,REDIS_CACHE_SERVICE,SimpleTenantMiddleware,StrictTenantMiddleware,TENANCY_CONFIG_SERVICE,TENANCY_STRATEGY_SERVICE,TenantMiddleware,createDatabaseCacheStorage,createSchemaFromModel,getCurrentTenant,getTenancyConfig,getTenancyStrategy,withTenant};
@@ -35,7 +35,11 @@ export declare class KeyvRedis extends EventEmitter implements KeyvStoreAdapter
35
35
  */
36
36
  noNamespaceAffectsAll?: boolean;
37
37
  });
38
- get client(): Bun.RedisClient;
38
+ /**
39
+ * Resolves the Redis client on first use. The async path is always safe, even when the
40
+ * Redis module has not been initialised yet (lazy boot).
41
+ */
42
+ protected getClient(): Promise<Bun.RedisClient>;
39
43
  /**
40
44
  * Get the options for the adapter.
41
45
  */
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Configuration of the {@link MnemosyneModule}.
3
+ * @author Axel Nana <axel.nana@workbud.com>
4
+ */
5
+ export type MnemosyneModuleConfig = {
6
+ cache: {
7
+ /** The Redis-backed cache. Set to `false` to not provide `Cache.redis`. */
8
+ redis?: {
9
+ /** The name of the Redis connection to use. */
10
+ connection: string;
11
+ } | false;
12
+ /** The memory cache. Set to `false` to not provide `Cache.memory`. */
13
+ memory?: {
14
+ /** The default TTL of entries, in milliseconds. */
15
+ ttl: number;
16
+ /** The maximum number of entries kept. */
17
+ lruSize: number;
18
+ } | false;
19
+ };
20
+ };
21
+ declare const MnemosyneModule_base: (abstract new () => {
22
+ readonly config: MnemosyneModuleConfig;
23
+ onInit(): Promise<void> | void;
24
+ onDestroy(): Promise<void> | void;
25
+ onServe(_app: import("elysia").default<any>): Promise<void> | void;
26
+ }) & {
27
+ readonly config: string;
28
+ with(config: Partial<MnemosyneModuleConfig> | (() => Partial<MnemosyneModuleConfig> | Promise<Partial<MnemosyneModuleConfig>>)): import("@elysiumjs/core").ConfiguredModule<MnemosyneModuleConfig>;
29
+ resolveConfig(input?: Partial<MnemosyneModuleConfig> | undefined): MnemosyneModuleConfig;
30
+ register<TConfig_1 = any>(props?: import("@elysiumjs/core").ModuleProps<TConfig_1>): (target: import("@elysiumjs/core").ModuleClass) => void;
31
+ configurable<TConfig extends object>(options?: {
32
+ defaults?: Partial<TConfig>;
33
+ required?: (keyof TConfig)[];
34
+ }): any;
35
+ };
36
+ /**
37
+ * Provides the caches as the global services `mnemosyne.cache.redis` and
38
+ * `mnemosyne.cache.memory`. Tenancy config and strategy, and database connections, are
39
+ * registered by the database driver modules.
40
+ * @author Axel Nana <axel.nana@workbud.com>
41
+ */
42
+ export declare class MnemosyneModule extends MnemosyneModule_base {
43
+ }
44
+ export {};
package/dist/model.d.ts CHANGED
@@ -7,7 +7,7 @@ import type { ColumnMetadata, ModelAdapter } from './interfaces';
7
7
  * @returns A validation schema based on the given columns.
8
8
  */
9
9
  export declare const createSchemaFromModel: (columns: ColumnMetadata[], { mode }?: {
10
- mode?: "create" | "update" | "select";
10
+ mode?: 'create' | 'update' | 'select';
11
11
  }) => import("@sinclair/typebox").TObject<{}>;
12
12
  /**
13
13
  * Mixin used to create a new abstract model class.
package/dist/tenancy.d.ts CHANGED
@@ -65,24 +65,23 @@ export interface ModelTenancyConfig {
65
65
  */
66
66
  policyName?: string;
67
67
  }
68
+ /** Name of the global service holding the {@link TenancyConfig}, registered by driver modules. */
69
+ export declare const TENANCY_CONFIG_SERVICE = "mnemosyne.tenancy.config";
70
+ /** Name of the global service holding the {@link TenancyStrategy}, registered by driver modules. */
71
+ export declare const TENANCY_STRATEGY_SERVICE = "mnemosyne.tenancy.strategy";
68
72
  /**
69
73
  * Gets the current global tenancy configuration.
70
74
  *
71
- * Reads from the 'elysium:mnemosyne' app config key. Returns a default
72
- * configuration if the app is not yet initialized or no tenancy config is set.
75
+ * Reads the global service `mnemosyne.tenancy.config`. Returns a default
76
+ * configuration if none is registered.
73
77
  *
74
78
  * @author Axel Nana <axel.nana@workbud.com>
75
79
  * @returns The current tenancy configuration.
76
80
  */
77
- export declare const getConfig: () => TenancyConfig;
81
+ export declare const getTenancyConfig: () => TenancyConfig;
78
82
  /**
79
- * Registers the tenancy strategy to use for table resolution and isolation.
80
- * @author Axel Nana <axel.nana@workbud.com>
81
- * @param strategy The tenancy strategy to register.
82
- */
83
- export declare const registerTenancyStrategy: (strategy: TenancyStrategy<unknown, unknown>) => void;
84
- /**
85
- * Gets the currently registered tenancy strategy.
83
+ * Gets the tenancy strategy registered by the driver module as the global service
84
+ * `mnemosyne.tenancy.strategy`.
86
85
  * @author Axel Nana <axel.nana@workbud.com>
87
86
  * @returns The current tenancy strategy, or `null` if none is registered.
88
87
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elysiumjs/mnemosyne",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Database and caching for Elysium.js applications",
6
6
  "private": false,
@@ -13,7 +13,7 @@
13
13
  "scripts": {
14
14
  "format": "prettier --write \"./src/**/*.ts\"",
15
15
  "clean": "rimraf dist",
16
- "build:js": "bun build ./src/index.ts --outdir ./dist --target bun --packages external --root ./src --minify",
16
+ "build:js": "bun build ./src/index.ts --outdir ./dist --target bun --packages external --root ./src --minify-syntax --minify-whitespace",
17
17
  "build:ts": "tsc",
18
18
  "build": "bun run clean && bun run build:js && bun run build:ts",
19
19
  "typecheck": "tsc --noEmit",
@@ -30,16 +30,15 @@
30
30
  "types": "dist/index.d.ts",
31
31
  "sideEffects": false,
32
32
  "peerDependencies": {
33
- "@elysiumjs/core": "^0.7.0"
33
+ "@elysiumjs/core": "^0.8.0"
34
34
  },
35
35
  "devDependencies": {
36
- "@elysiumjs/core": "0.7.0"
36
+ "@elysiumjs/core": "0.8.0"
37
37
  },
38
38
  "dependencies": {
39
- "cache-manager": "^7.2.3",
40
- "cacheable": "^2.0.3",
41
- "keyv": "^5.0.0",
42
- "radash": "^12.1.1",
43
- "reflect-metadata": "^0.2.2"
39
+ "cache-manager": "^7.2.9",
40
+ "cacheable": "^2.5.0",
41
+ "keyv": "^5.6.0",
42
+ "radash": "^12.1.1"
44
43
  }
45
44
  }
package/src/cache.ts CHANGED
@@ -14,12 +14,16 @@
14
14
 
15
15
  import type { KeyvStoreAdapter } from 'keyv';
16
16
 
17
+ import { Service, ServiceNotFoundError } from '@elysiumjs/core';
17
18
  import { createCache } from 'cache-manager';
18
- import { KeyvCacheableMemory } from 'cacheable';
19
19
  import { Keyv } from 'keyv';
20
20
  import { alphabetical } from 'radash';
21
21
 
22
- import { KeyvRedis } from './keyv-redis';
22
+ /** Name of the global service backing `Cache.redis`. */
23
+ export const REDIS_CACHE_SERVICE = 'mnemosyne.cache.redis';
24
+
25
+ /** Name of the global service backing `Cache.memory`. */
26
+ export const MEMORY_CACHE_SERVICE = 'mnemosyne.cache.memory';
23
27
 
24
28
  /**
25
29
  * Interface for a cache storage.
@@ -118,7 +122,7 @@ export interface CacheInterface {
118
122
  * @param store The cache storage to use in the interface.
119
123
  * @returns A new `CacheInterface`.
120
124
  */
121
- const makeCacheInterface = (store: KeyvStoreAdapter): CacheInterface => {
125
+ export const makeCacheInterface = (store: KeyvStoreAdapter): CacheInterface => {
122
126
  const base = createCache({
123
127
  stores: [
124
128
  new Keyv({
@@ -154,10 +158,17 @@ const makeCacheInterface = (store: KeyvStoreAdapter): CacheInterface => {
154
158
  };
155
159
  };
156
160
 
157
- export abstract class Cache {
158
- static #redis: CacheInterface | undefined;
159
- static #memory: CacheInterface | undefined;
161
+ const getCache = (name: string): CacheInterface => {
162
+ const cache = Service.get<CacheInterface>(name);
163
+ if (!cache) throw new ServiceNotFoundError(name);
164
+ return cache;
165
+ };
160
166
 
167
+ /**
168
+ * Static access to the caches provided by the `MnemosyneModule`.
169
+ * @author Axel Nana <axel.nana@workbud.com>
170
+ */
171
+ export abstract class Cache {
161
172
  /**
162
173
  * A weak cache storage for simple non-intrusive caching of data.
163
174
  * @author Axel Nana <axel.nana@workbud.com>
@@ -191,28 +202,18 @@ export abstract class Cache {
191
202
  /**
192
203
  * Redis-based cache storage.
193
204
  * @author Axel Nana <axel.nana@workbud.com>
205
+ * @throws {ServiceNotFoundError} If the `MnemosyneModule` is not loaded or has no redis cache.
194
206
  */
195
207
  public static get redis(): CacheInterface {
196
- if (!Cache.#redis) {
197
- Cache.#redis = makeCacheInterface(
198
- new KeyvRedis({
199
- connection: 'cache'
200
- })
201
- );
202
- }
203
-
204
- return Cache.#redis;
208
+ return getCache(REDIS_CACHE_SERVICE);
205
209
  }
206
210
 
207
211
  /**
208
212
  * Memory-based cache storage.
209
213
  * @author Axel Nana <axel.nana@workbud.com>
214
+ * @throws {ServiceNotFoundError} If the `MnemosyneModule` is not loaded or has no memory cache.
210
215
  */
211
216
  public static get memory(): CacheInterface {
212
- if (!Cache.#memory) {
213
- Cache.#memory = makeCacheInterface(new KeyvCacheableMemory({ ttl: 60000, lruSize: 5000 }));
214
- }
215
-
216
- return Cache.#memory;
217
+ return getCache(MEMORY_CACHE_SERVICE);
217
218
  }
218
219
  }
package/src/database.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  import type { CacheInterface } from './cache';
16
16
  import type { DatabaseCacheConfig, DatabaseDriver } from './interfaces';
17
17
 
18
- import { Application, Service } from '@elysiumjs/core';
18
+ import { Application, Service, ServiceNotFoundError } from '@elysiumjs/core';
19
19
 
20
20
  import { Cache } from './cache';
21
21
 
@@ -35,8 +35,8 @@ export const createDatabaseCacheStorage = (
35
35
  /**
36
36
  * Abstract database manager.
37
37
  *
38
- * Manages named database connections using a driver. Connections are
39
- * registered in the service container and can be retrieved by name.
38
+ * Retrieves named database connections using a driver. Connections are registered as
39
+ * global services `db.connection.{name}` by the driver modules.
40
40
  *
41
41
  * @author Axel Nana <axel.nana@workbud.com>
42
42
  * @template TConnection The connection type managed by the driver.
@@ -49,11 +49,13 @@ export abstract class AbstractDatabase<TConnection, TConnectionConfig> {
49
49
  protected abstract driver: DatabaseDriver<TConnection, TConnectionConfig>;
50
50
 
51
51
  /**
52
- * Creates a service name for a database connection.
52
+ * Creates a service name for a database connection. Driver modules register their
53
+ * connections (and the `default` alias) under this name.
54
+ * @author Axel Nana <axel.nana@workbud.com>
53
55
  * @param name The name of the connection.
54
56
  * @returns A service name for the connection.
55
57
  */
56
- private getConnectionName(name: string): string {
58
+ public static getConnectionName(name: string): string {
57
59
  return `db.connection.${name}`;
58
60
  }
59
61
 
@@ -62,6 +64,7 @@ export abstract class AbstractDatabase<TConnection, TConnectionConfig> {
62
64
  *
63
65
  * For the default connection, checks the async context for a
64
66
  * transaction handle (`db:tx`) before falling back to the service container.
67
+ * @throws {ServiceNotFoundError} If no connection with that name is registered.
65
68
  *
66
69
  * @author Axel Nana <axel.nana@workbud.com>
67
70
  * @param name The connection name.
@@ -73,39 +76,11 @@ export abstract class AbstractDatabase<TConnection, TConnectionConfig> {
73
76
  if (tx) return tx;
74
77
  }
75
78
 
76
- const serviceName = this.getConnectionName(name);
77
- if (!Service.exists(serviceName)) {
78
- // TODO: Use logger service here
79
- console.error(
80
- `No connection with name ${name} found. Please make sure to register the connection before using it.`
81
- );
82
- process.exit(1);
83
- }
84
-
85
- return Service.get<TConnection>(serviceName)!;
86
- }
87
-
88
- /**
89
- * Creates and registers a new connection.
90
- *
91
- * This will make the registered connection available for dependency injection with
92
- * the key `db.connection.{name}`, where `{name}` is replaced with the given name.
93
- *
94
- * @author Axel Nana <axel.nana@workbud.com>
95
- * @param name The connection name.
96
- * @param config The connection configuration.
97
- * @returns The newly created and registered connection.
98
- */
99
- public registerConnection(name: string, config: TConnectionConfig): TConnection {
100
- const serviceName = this.getConnectionName(name);
101
- if (Service.exists(serviceName)) {
102
- // TODO: Use logger service here
103
- console.error(`A connection with the name ${name} has already been registered.`);
104
- process.exit(1);
105
- }
79
+ const serviceName = AbstractDatabase.getConnectionName(name);
80
+ const connection = Service.get<TConnection>(serviceName);
81
+ if (connection === null) throw new ServiceNotFoundError(serviceName);
106
82
 
107
- const connection = this.driver.createConnection(config);
108
- return Service.instance(serviceName, connection);
83
+ return connection;
109
84
  }
110
85
 
111
86
  /**
@@ -115,7 +90,7 @@ export abstract class AbstractDatabase<TConnection, TConnectionConfig> {
115
90
  * @returns `true` if the connection exists, `false` otherwise.
116
91
  */
117
92
  public connectionExists(name: string): boolean {
118
- return Service.exists(this.getConnectionName(name));
93
+ return Service.exists(AbstractDatabase.getConnectionName(name));
119
94
  }
120
95
 
121
96
  /**
@@ -127,18 +102,6 @@ export abstract class AbstractDatabase<TConnection, TConnectionConfig> {
127
102
  return this.getConnection('default');
128
103
  }
129
104
 
130
- /**
131
- * Sets the default connection.
132
- * @author Axel Nana <axel.nana@workbud.com>
133
- * @param name The name of the connection to set as default.
134
- * @returns The default connection.
135
- */
136
- public setDefaultConnection(name: string): TConnection {
137
- const serviceName = this.getConnectionName('default');
138
- Service.remove(serviceName);
139
- return Service.instance(serviceName, this.getConnection(name));
140
- }
141
-
142
105
  /**
143
106
  * Returns the raw underlying client for the given connection.
144
107
  * @author Axel Nana <axel.nana@workbud.com>