@elysiumjs/mnemosyne 0.1.2 → 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 +8 -0
- package/CHANGELOG.md +34 -0
- package/README.md +19 -16
- package/dist/cache.d.ts +130 -0
- package/dist/database.d.ts +66 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +2 -0
- package/dist/interfaces.d.ts +350 -0
- package/dist/keyv-redis.d.ts +88 -0
- package/dist/mnemosyne.module.d.ts +44 -0
- package/dist/model.d.ts +68 -0
- package/dist/repository.d.ts +96 -0
- package/dist/tenancy.d.ts +179 -0
- package/package.json +8 -9
- package/src/cache.ts +21 -20
- package/src/database.ts +13 -50
- package/src/index.ts +2 -12
- package/src/keyv-redis.ts +39 -26
- package/src/mnemosyne.module.ts +83 -0
- package/src/tenancy.ts +12 -29
- 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/bunfig.toml +0 -19
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
$ bun run clean && bun run build:js && bun run build:ts
|
|
2
|
+
$ rimraf dist
|
|
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
|
+
|
|
6
|
+
index.js 11.16 KB (entry point)
|
|
7
|
+
|
|
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
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
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";
|
|
6
|
+
/**
|
|
7
|
+
* Interface for a cache storage.
|
|
8
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
9
|
+
*/
|
|
10
|
+
export interface CacheInterface {
|
|
11
|
+
/**
|
|
12
|
+
* Retrieves the cached data for a given key.
|
|
13
|
+
* @typeParam T The type of the cached data.
|
|
14
|
+
*
|
|
15
|
+
* @param key The key of the cached data.
|
|
16
|
+
*/
|
|
17
|
+
get<T>(key: string): Promise<T | undefined>;
|
|
18
|
+
/**
|
|
19
|
+
* Retrieves the cached data for a given list of keys.
|
|
20
|
+
* @typeParam T The type of the cached data.
|
|
21
|
+
*
|
|
22
|
+
* @param keys The list of cached data keys.
|
|
23
|
+
*/
|
|
24
|
+
mget<T>(keys: string[]): Promise<Array<T | undefined>>;
|
|
25
|
+
/**
|
|
26
|
+
* Gets the TTL of the cached data with the given key.
|
|
27
|
+
* @param key The key of the cached data.
|
|
28
|
+
* @returns The TTL of the cached data, or `undefined` if the data does not exist.
|
|
29
|
+
*/
|
|
30
|
+
ttl(key: string): Promise<number | undefined>;
|
|
31
|
+
/**
|
|
32
|
+
* Caches data under the given key and an optional TTL.
|
|
33
|
+
* @typeParam T The type of the data to cache.
|
|
34
|
+
*
|
|
35
|
+
* @param key The key of the cached data.
|
|
36
|
+
* @param data The data to cache.
|
|
37
|
+
* @param ttl The optional TTL of the cached data. If not defined, the data is cached forever.
|
|
38
|
+
* @returns The cached data.
|
|
39
|
+
*/
|
|
40
|
+
set<T>(key: string, data: T, ttl?: number): Promise<T>;
|
|
41
|
+
/**
|
|
42
|
+
* Caches data under the given list of keys and an optional TTL.
|
|
43
|
+
* @typeParam T The type of the data to cache.
|
|
44
|
+
*
|
|
45
|
+
* @param data The list of data to cache.
|
|
46
|
+
* @returns The list of cached data.
|
|
47
|
+
*/
|
|
48
|
+
mset<T>(data: Array<{
|
|
49
|
+
key: string;
|
|
50
|
+
value: T;
|
|
51
|
+
ttl?: number;
|
|
52
|
+
}>): Promise<Array<{
|
|
53
|
+
key: string;
|
|
54
|
+
value: T;
|
|
55
|
+
ttl?: number;
|
|
56
|
+
}>>;
|
|
57
|
+
/**
|
|
58
|
+
* Deletes the cached data with the given key.
|
|
59
|
+
* @typeParam T The type of the data to cache.
|
|
60
|
+
*
|
|
61
|
+
* @param key The key of the cached data.
|
|
62
|
+
* @returns `true` if the data was deleted, `false` otherwise.
|
|
63
|
+
*/
|
|
64
|
+
del(key: string): Promise<boolean>;
|
|
65
|
+
/**
|
|
66
|
+
* Deletes the cached data with the given list of keys.
|
|
67
|
+
* @typeParam T The type of the data to cache.
|
|
68
|
+
*
|
|
69
|
+
* @param keys The list of cached data keys.
|
|
70
|
+
* @returns `true` if the data was deleted, `false` otherwise.
|
|
71
|
+
*/
|
|
72
|
+
mdel(keys: string[]): Promise<boolean>;
|
|
73
|
+
/**
|
|
74
|
+
* Deletes all cached data.
|
|
75
|
+
* @returns `true` if the data was deleted, `false` otherwise.
|
|
76
|
+
*/
|
|
77
|
+
clear(): Promise<boolean>;
|
|
78
|
+
/**
|
|
79
|
+
* Creates a sub-cache with the given tags.
|
|
80
|
+
* @param tags The list of tags for the sub-cache.
|
|
81
|
+
*/
|
|
82
|
+
tags(...tags: string[]): Omit<CacheInterface, 'tags'>;
|
|
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
|
+
*/
|
|
95
|
+
export declare abstract class Cache {
|
|
96
|
+
/**
|
|
97
|
+
* A weak cache storage for simple non-intrusive caching of data.
|
|
98
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
99
|
+
*/
|
|
100
|
+
static readonly weak: {
|
|
101
|
+
/**
|
|
102
|
+
* INTERNAL USAGE ONLY.
|
|
103
|
+
* @private
|
|
104
|
+
*/
|
|
105
|
+
map: WeakMap<object, unknown>;
|
|
106
|
+
/**
|
|
107
|
+
* Gets the cached data for a given context.
|
|
108
|
+
* @param ctx The cache context.
|
|
109
|
+
* @returns The cached data.
|
|
110
|
+
*/
|
|
111
|
+
get<T extends object>(ctx: object): T;
|
|
112
|
+
/**
|
|
113
|
+
* Deletes the cached data for a given context.
|
|
114
|
+
* @param ctx The cache context.
|
|
115
|
+
*/
|
|
116
|
+
del(ctx: object): void;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Redis-based cache storage.
|
|
120
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
121
|
+
* @throws {ServiceNotFoundError} If the `MnemosyneModule` is not loaded or has no redis cache.
|
|
122
|
+
*/
|
|
123
|
+
static get redis(): CacheInterface;
|
|
124
|
+
/**
|
|
125
|
+
* Memory-based cache storage.
|
|
126
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
127
|
+
* @throws {ServiceNotFoundError} If the `MnemosyneModule` is not loaded or has no memory cache.
|
|
128
|
+
*/
|
|
129
|
+
static get memory(): CacheInterface;
|
|
130
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { CacheInterface } from './cache';
|
|
2
|
+
import type { DatabaseCacheConfig, DatabaseDriver } from './interfaces';
|
|
3
|
+
/**
|
|
4
|
+
* Creates a cache storage instance tagged for database use.
|
|
5
|
+
* Drivers can use this to build their cache backends.
|
|
6
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
7
|
+
* @param config The database cache configuration.
|
|
8
|
+
* @returns A cache interface tagged with 'database'.
|
|
9
|
+
*/
|
|
10
|
+
export declare const createDatabaseCacheStorage: (config: DatabaseCacheConfig) => Omit<CacheInterface, 'tags'>;
|
|
11
|
+
/**
|
|
12
|
+
* Abstract database manager.
|
|
13
|
+
*
|
|
14
|
+
* Retrieves named database connections using a driver. Connections are registered as
|
|
15
|
+
* global services `db.connection.{name}` by the driver modules.
|
|
16
|
+
*
|
|
17
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
18
|
+
* @template TConnection The connection type managed by the driver.
|
|
19
|
+
* @template TConnectionConfig The configuration used to create a connection.
|
|
20
|
+
*/
|
|
21
|
+
export declare abstract class AbstractDatabase<TConnection, TConnectionConfig> {
|
|
22
|
+
/**
|
|
23
|
+
* The database driver used to create and manage connections.
|
|
24
|
+
*/
|
|
25
|
+
protected abstract driver: DatabaseDriver<TConnection, TConnectionConfig>;
|
|
26
|
+
/**
|
|
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>
|
|
30
|
+
* @param name The name of the connection.
|
|
31
|
+
* @returns A service name for the connection.
|
|
32
|
+
*/
|
|
33
|
+
static getConnectionName(name: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* Retrieves the connection with the given name.
|
|
36
|
+
*
|
|
37
|
+
* For the default connection, checks the async context for a
|
|
38
|
+
* transaction handle (`db:tx`) before falling back to the service container.
|
|
39
|
+
* @throws {ServiceNotFoundError} If no connection with that name is registered.
|
|
40
|
+
*
|
|
41
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
42
|
+
* @param name The connection name.
|
|
43
|
+
* @returns The connection with the given name.
|
|
44
|
+
*/
|
|
45
|
+
getConnection(name?: string): TConnection;
|
|
46
|
+
/**
|
|
47
|
+
* Checks if a connection with the given name exists.
|
|
48
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
49
|
+
* @param name The name of the connection to check.
|
|
50
|
+
* @returns `true` if the connection exists, `false` otherwise.
|
|
51
|
+
*/
|
|
52
|
+
connectionExists(name: string): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Retrieves the default connection.
|
|
55
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
56
|
+
* @returns The default connection.
|
|
57
|
+
*/
|
|
58
|
+
getDefaultConnection(): TConnection;
|
|
59
|
+
/**
|
|
60
|
+
* Returns the raw underlying client for the given connection.
|
|
61
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
62
|
+
* @param name The connection name.
|
|
63
|
+
* @returns The raw client instance.
|
|
64
|
+
*/
|
|
65
|
+
getRawClient(name?: string): unknown;
|
|
66
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type { ColumnMetadata, DatabaseCacheConfig, DatabaseCacheStrategy, DatabaseDriver, IdType, ModelAdapter, ModelClass, RepositoryClass, RepositoryInterface, TenancyStrategy } from './interfaces';
|
|
2
|
+
export * from './database';
|
|
3
|
+
export * from './model';
|
|
4
|
+
export * from './repository';
|
|
5
|
+
export * from './tenancy';
|
|
6
|
+
export type { TenancyConfig, TenancyMode, ModelTenancyConfig } from './tenancy';
|
|
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
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// @bun
|
|
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};
|