@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.
- package/.turbo/turbo-build.log +3 -3
- package/CHANGELOG.md +34 -0
- package/README.md +19 -16
- package/dist/cache.d.ts +18 -1
- package/dist/database.d.ts +8 -24
- package/dist/index.d.ts +7 -15
- package/dist/index.js +1 -1
- package/dist/keyv-redis.d.ts +5 -1
- package/dist/mnemosyne.module.d.ts +44 -0
- package/dist/model.d.ts +1 -1
- package/dist/tenancy.d.ts +9 -10
- package/package.json +8 -9
- package/src/cache.ts +21 -20
- package/src/database.ts +13 -50
- package/src/index.ts +7 -27
- package/src/keyv-redis.ts +39 -26
- package/src/mnemosyne.module.ts +83 -0
- package/src/tenancy.ts +13 -30
- package/tests/cache.test.ts +51 -10
- package/tests/database.test.ts +37 -173
- package/tests/mnemosyne.module.test.ts +214 -0
- package/tests/setup.ts +0 -13
- package/.turbo/turbo-typecheck.log +0 -1
- package/coverage/.lcov.info.2cbccf8710225f8a.tmp +0 -301
- package/coverage/.lcov.info.80cb03b14eb4a841.tmp +0 -644
- package/coverage/lcov.info +0 -549
package/.turbo/turbo-build.log
CHANGED
|
@@ -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
|
|
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
|
|
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 {
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
}
|
package/dist/database.d.ts
CHANGED
|
@@ -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,
|
|
10
|
+
export declare const createDatabaseCacheStorage: (config: DatabaseCacheConfig) => Omit<CacheInterface, 'tags'>;
|
|
11
11
|
/**
|
|
12
12
|
* Abstract database manager.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
-
|
|
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
|
|
4
|
-
export
|
|
5
|
-
export
|
|
6
|
-
export
|
|
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
|
|
10
|
-
export
|
|
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};
|
package/dist/keyv-redis.d.ts
CHANGED
|
@@ -35,7 +35,11 @@ export declare class KeyvRedis extends EventEmitter implements KeyvStoreAdapter
|
|
|
35
35
|
*/
|
|
36
36
|
noNamespaceAffectsAll?: boolean;
|
|
37
37
|
});
|
|
38
|
-
|
|
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?:
|
|
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
|
|
72
|
-
* configuration if
|
|
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
|
|
81
|
+
export declare const getTenancyConfig: () => TenancyConfig;
|
|
78
82
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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.
|
|
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.
|
|
33
|
+
"@elysiumjs/core": "^0.8.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@elysiumjs/core": "0.
|
|
36
|
+
"@elysiumjs/core": "0.8.0"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"cache-manager": "^7.2.
|
|
40
|
-
"cacheable": "^2.0
|
|
41
|
-
"keyv": "^5.
|
|
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
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
39
|
-
*
|
|
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
|
-
|
|
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 =
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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(
|
|
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>
|