@elysiumjs/mnemosyne 0.1.2 → 0.3.1
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/CHANGELOG.md +90 -0
- package/README.md +19 -16
- package/dist/cache.d.ts +134 -0
- package/dist/database.d.ts +127 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +2 -0
- package/dist/interfaces.d.ts +360 -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 +201 -0
- package/package.json +21 -12
- package/bunfig.toml +0 -19
- package/src/cache.ts +0 -218
- package/src/database.ts +0 -151
- package/src/index.ts +0 -47
- package/src/interfaces.ts +0 -449
- package/src/keyv-redis.ts +0 -313
- package/src/model.ts +0 -189
- package/src/repository.ts +0 -137
- package/src/tenancy.ts +0 -291
- package/tests/cache.test.ts +0 -464
- package/tests/database.test.ts +0 -249
- package/tests/model.test.ts +0 -234
- package/tests/repository.test.ts +0 -177
- package/tests/setup.ts +0 -26
- package/tsconfig.json +0 -12
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { Context } from '@elysiumjs/core';
|
|
2
|
+
import type { TenancyStrategy } from './interfaces';
|
|
3
|
+
import { Middleware } from '@elysiumjs/core';
|
|
4
|
+
/**
|
|
5
|
+
* The tenancy mode for the application.
|
|
6
|
+
* - `schema`: Each tenant has their own Postgres schema.
|
|
7
|
+
* - `rls`: Row-Level Security with a shared schema and a tenant column.
|
|
8
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
9
|
+
*/
|
|
10
|
+
export type TenancyMode = 'schema' | 'rls';
|
|
11
|
+
/**
|
|
12
|
+
* Global tenancy configuration for the application.
|
|
13
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
14
|
+
*/
|
|
15
|
+
export interface TenancyConfig {
|
|
16
|
+
/**
|
|
17
|
+
* The tenancy mode to use.
|
|
18
|
+
*/
|
|
19
|
+
mode: TenancyMode;
|
|
20
|
+
/**
|
|
21
|
+
* Configuration specific to RLS mode.
|
|
22
|
+
*/
|
|
23
|
+
rls?: {
|
|
24
|
+
/**
|
|
25
|
+
* The PostgreSQL session variable used to store the current tenant.
|
|
26
|
+
* @default 'app.current_tenant'
|
|
27
|
+
*/
|
|
28
|
+
sessionVariable?: string;
|
|
29
|
+
/**
|
|
30
|
+
* The default column name used for tenant isolation in RLS policies.
|
|
31
|
+
* @default 'tenant_id'
|
|
32
|
+
*/
|
|
33
|
+
defaultColumn?: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Configuration specific to schema-based tenancy.
|
|
37
|
+
*/
|
|
38
|
+
schema?: {
|
|
39
|
+
/**
|
|
40
|
+
* Whether to automatically create tenant schemas on first use.
|
|
41
|
+
*/
|
|
42
|
+
autoCreate?: boolean;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Per-model tenancy configuration, used to control how a specific model
|
|
47
|
+
* interacts with the tenancy system.
|
|
48
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
49
|
+
*/
|
|
50
|
+
export interface ModelTenancyConfig {
|
|
51
|
+
/**
|
|
52
|
+
* Whether tenancy is enabled for this model.
|
|
53
|
+
*/
|
|
54
|
+
enabled: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Override the global tenancy mode for this model.
|
|
57
|
+
*/
|
|
58
|
+
mode?: TenancyMode;
|
|
59
|
+
/**
|
|
60
|
+
* The column name used for RLS tenant isolation in this model.
|
|
61
|
+
*/
|
|
62
|
+
column?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Custom RLS policy name for this model.
|
|
65
|
+
*/
|
|
66
|
+
policyName?: string;
|
|
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";
|
|
72
|
+
/**
|
|
73
|
+
* Gets the current global tenancy configuration.
|
|
74
|
+
*
|
|
75
|
+
* Reads the global service `mnemosyne.tenancy.config`. Returns a default
|
|
76
|
+
* configuration if none is registered.
|
|
77
|
+
*
|
|
78
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
79
|
+
* @returns The current tenancy configuration.
|
|
80
|
+
*/
|
|
81
|
+
export declare const getTenancyConfig: () => TenancyConfig;
|
|
82
|
+
/**
|
|
83
|
+
* Gets the tenancy strategy registered by the driver module as the global service
|
|
84
|
+
* `mnemosyne.tenancy.strategy`.
|
|
85
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
86
|
+
* @returns The current tenancy strategy, or `null` if none is registered.
|
|
87
|
+
*/
|
|
88
|
+
export declare const getTenancyStrategy: () => TenancyStrategy<unknown, unknown> | null;
|
|
89
|
+
/**
|
|
90
|
+
* Gets the current tenant from the async context.
|
|
91
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
92
|
+
* @returns The current tenant identifier, or `null` if not set.
|
|
93
|
+
*/
|
|
94
|
+
export declare const getCurrentTenant: () => string | null;
|
|
95
|
+
/**
|
|
96
|
+
* Runs a callback within the context of a specific tenant.
|
|
97
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
98
|
+
* @param tenant The tenant identifier to set.
|
|
99
|
+
* @param callback The function to execute within the tenant context.
|
|
100
|
+
* @returns The return value of the callback.
|
|
101
|
+
*/
|
|
102
|
+
export declare const withTenant: <T>(tenant: string, callback: () => T) => T;
|
|
103
|
+
/**
|
|
104
|
+
* The pattern a tenant identifier must match: 1 to 63 ASCII letters, digits, underscores or dashes,
|
|
105
|
+
* not starting with `pg_` and other than `information_schema` (in any case).
|
|
106
|
+
*
|
|
107
|
+
* Tenant identifiers become PostgreSQL schema names in schema mode, and 63 bytes is the PostgreSQL
|
|
108
|
+
* identifier limit. The pattern leaves out quotes, dots, spaces and other characters that would
|
|
109
|
+
* need escaping or change how an identifier is read, and the names of PostgreSQL system schemas.
|
|
110
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
111
|
+
*/
|
|
112
|
+
export declare const TENANT_ID_PATTERN: RegExp;
|
|
113
|
+
/**
|
|
114
|
+
* Checks that a tenant identifier is well-formed (see {@link TENANT_ID_PATTERN}). This does not
|
|
115
|
+
* check that the tenant exists.
|
|
116
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
117
|
+
* @param tenant The tenant identifier.
|
|
118
|
+
* @returns `true` if the identifier is well-formed, `false` otherwise.
|
|
119
|
+
*/
|
|
120
|
+
export declare const isValidTenantId: (tenant: string) => boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Base class for tenant-aware middleware.
|
|
123
|
+
*
|
|
124
|
+
* The middleware extracts the tenant identifier from an HTTP header and
|
|
125
|
+
* sets `ctx.tenant`. Identifiers must match {@link TENANT_ID_PATTERN} and be accepted by
|
|
126
|
+
* `validateTenant`, which rejects every tenant unless overridden. For RLS mode, route handlers should use the driver's
|
|
127
|
+
* isolation mechanism to wrap their queries in a tenant-scoped transaction.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```typescript
|
|
131
|
+
* class OrganizationMiddleware extends TenantMiddleware {
|
|
132
|
+
* protected header = 'x-organization-id';
|
|
133
|
+
* protected allowPublic = false;
|
|
134
|
+
*
|
|
135
|
+
* protected async validateTenant(orgId: string): Promise<boolean> {
|
|
136
|
+
* return Organization.exists(orgId);
|
|
137
|
+
* }
|
|
138
|
+
* }
|
|
139
|
+
* ```
|
|
140
|
+
*
|
|
141
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
142
|
+
*/
|
|
143
|
+
export declare abstract class TenantMiddleware extends Middleware {
|
|
144
|
+
/**
|
|
145
|
+
* The HTTP header name from which the tenant ID is read.
|
|
146
|
+
*/
|
|
147
|
+
protected header: string;
|
|
148
|
+
/**
|
|
149
|
+
* Whether requests without a tenant ID are allowed (defaulting to 'public').
|
|
150
|
+
*/
|
|
151
|
+
protected allowPublic: boolean;
|
|
152
|
+
/**
|
|
153
|
+
* The database connection name used for tenant operations.
|
|
154
|
+
*/
|
|
155
|
+
protected connectionName: string;
|
|
156
|
+
/**
|
|
157
|
+
* Validates that a tenant identifier is legitimate.
|
|
158
|
+
*
|
|
159
|
+
* Rejects every tenant by default: override it to check the tenant against your data store.
|
|
160
|
+
* Only identifiers matching {@link TENANT_ID_PATTERN} reach this method.
|
|
161
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
162
|
+
* @param _tenant The tenant identifier to validate.
|
|
163
|
+
* @returns `true` if the tenant is valid, `false` otherwise.
|
|
164
|
+
*/
|
|
165
|
+
protected validateTenant(_tenant: string): Promise<boolean>;
|
|
166
|
+
/**
|
|
167
|
+
* Hook called after a tenant has been successfully resolved.
|
|
168
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
169
|
+
* @param _tenant The resolved tenant identifier.
|
|
170
|
+
* @param _ctx The request context.
|
|
171
|
+
*/
|
|
172
|
+
protected onTenantResolved(_tenant: string, _ctx: Context): Promise<void>;
|
|
173
|
+
/**
|
|
174
|
+
* Called when the tenant cannot be determined or is invalid.
|
|
175
|
+
* Throws an appropriate error by default.
|
|
176
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
177
|
+
* @param reason Whether the tenant is 'missing' or 'invalid'.
|
|
178
|
+
* @param _ctx The request context.
|
|
179
|
+
*/
|
|
180
|
+
protected onTenantError(reason: 'missing' | 'invalid', _ctx: Context): never;
|
|
181
|
+
/**
|
|
182
|
+
* Extracts and validates the tenant from the incoming request.
|
|
183
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
184
|
+
* @param ctx The request context.
|
|
185
|
+
*/
|
|
186
|
+
onRequest(ctx: Context): Promise<any>;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* A simple tenant middleware that allows public access by default. Requests naming a tenant are
|
|
190
|
+
* rejected until `validateTenant` is overridden.
|
|
191
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
192
|
+
*/
|
|
193
|
+
export declare class SimpleTenantMiddleware extends TenantMiddleware {
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* A strict tenant middleware that rejects requests without a tenant ID.
|
|
197
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
198
|
+
*/
|
|
199
|
+
export declare class StrictTenantMiddleware extends TenantMiddleware {
|
|
200
|
+
protected allowPublic: boolean;
|
|
201
|
+
}
|
package/package.json
CHANGED
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elysiumjs/mnemosyne",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Database and caching for Elysium.js applications",
|
|
6
6
|
"private": false,
|
|
7
7
|
"author": "Workbud Technologies Inc.",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "https://github.com/
|
|
10
|
+
"url": "git+https://github.com/na2axl/elysiumjs.git",
|
|
11
|
+
"directory": "@elysiumjs/mnemosyne"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/na2axl/elysiumjs/tree/main/@elysiumjs/mnemosyne#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/na2axl/elysiumjs/issues"
|
|
11
16
|
},
|
|
12
|
-
"homepage": "https://github.com/workbud/elysium",
|
|
13
17
|
"scripts": {
|
|
14
18
|
"format": "prettier --write \"./src/**/*.ts\"",
|
|
15
19
|
"clean": "rimraf dist",
|
|
16
|
-
"build:js": "bun build ./src/index.ts --outdir ./dist --target bun --packages external --root ./src --minify",
|
|
20
|
+
"build:js": "bun build ./src/index.ts --outdir ./dist --target bun --packages external --root ./src --minify-syntax --minify-whitespace",
|
|
17
21
|
"build:ts": "tsc",
|
|
18
22
|
"build": "bun run clean && bun run build:js && bun run build:ts",
|
|
19
23
|
"typecheck": "tsc --noEmit",
|
|
20
|
-
"prepublishOnly": "bun run build"
|
|
24
|
+
"prepublishOnly": "bun run build",
|
|
25
|
+
"test": "bun test",
|
|
26
|
+
"test:coverage": "bun test --coverage"
|
|
21
27
|
},
|
|
22
28
|
"keywords": [
|
|
23
29
|
"elysium",
|
|
@@ -28,18 +34,21 @@
|
|
|
28
34
|
"module": "dist/index.js",
|
|
29
35
|
"type": "module",
|
|
30
36
|
"types": "dist/index.d.ts",
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"CHANGELOG.md"
|
|
40
|
+
],
|
|
31
41
|
"sideEffects": false,
|
|
32
42
|
"peerDependencies": {
|
|
33
|
-
"@elysiumjs/core": "
|
|
43
|
+
"@elysiumjs/core": "^0.10.0"
|
|
34
44
|
},
|
|
35
45
|
"devDependencies": {
|
|
36
|
-
"@elysiumjs/core": "
|
|
46
|
+
"@elysiumjs/core": "0.10.0"
|
|
37
47
|
},
|
|
38
48
|
"dependencies": {
|
|
39
|
-
"cache-manager": "^7.2.
|
|
40
|
-
"cacheable": "^2.0
|
|
41
|
-
"keyv": "^5.
|
|
42
|
-
"radash": "
|
|
43
|
-
"reflect-metadata": "catalog:"
|
|
49
|
+
"cache-manager": "^7.2.9",
|
|
50
|
+
"cacheable": "^2.5.0",
|
|
51
|
+
"keyv": "^5.6.0",
|
|
52
|
+
"radash": "^12.1.1"
|
|
44
53
|
}
|
|
45
54
|
}
|
package/bunfig.toml
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
[test]
|
|
2
|
-
# The root directory of all unit tests
|
|
3
|
-
root = "./tests"
|
|
4
|
-
# A file loaded right before running `bun test`
|
|
5
|
-
preload = ["./tests/setup.ts"]
|
|
6
|
-
# Enable code coverage by default
|
|
7
|
-
coverage = true
|
|
8
|
-
# Requires 90% line coverage, 70% function coverage, and 80% statement coverage
|
|
9
|
-
coverageThreshold = { line = 0.9, function = 0.7, statement = 0.8 }
|
|
10
|
-
# Tests files are included in the coverage statistics
|
|
11
|
-
coverageSkipTestFiles = true
|
|
12
|
-
# Use lcov as the coverage reporter
|
|
13
|
-
coverageReporter = ["lcov", "text"]
|
|
14
|
-
# Path the output coverage results
|
|
15
|
-
coverageDir = "./coverage"
|
|
16
|
-
|
|
17
|
-
[run]
|
|
18
|
-
# Always use bun runtime for every `bun run` commands
|
|
19
|
-
bun = true
|
package/src/cache.ts
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025-present Workbud Technologies Inc. All rights reserved.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
import type { KeyvStoreAdapter } from 'keyv';
|
|
16
|
-
|
|
17
|
-
import { createCache } from 'cache-manager';
|
|
18
|
-
import { KeyvCacheableMemory } from 'cacheable';
|
|
19
|
-
import { Keyv } from 'keyv';
|
|
20
|
-
import { alphabetical } from 'radash';
|
|
21
|
-
|
|
22
|
-
import { KeyvRedis } from './keyv-redis';
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Interface for a cache storage.
|
|
26
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
27
|
-
*/
|
|
28
|
-
export interface CacheInterface {
|
|
29
|
-
/**
|
|
30
|
-
* Retrieves the cached data for a given key.
|
|
31
|
-
* @typeParam T The type of the cached data.
|
|
32
|
-
*
|
|
33
|
-
* @param key The key of the cached data.
|
|
34
|
-
*/
|
|
35
|
-
get<T>(key: string): Promise<T | undefined>;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Retrieves the cached data for a given list of keys.
|
|
39
|
-
* @typeParam T The type of the cached data.
|
|
40
|
-
*
|
|
41
|
-
* @param keys The list of cached data keys.
|
|
42
|
-
*/
|
|
43
|
-
mget<T>(keys: string[]): Promise<Array<T | undefined>>;
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Gets the TTL of the cached data with the given key.
|
|
47
|
-
* @param key The key of the cached data.
|
|
48
|
-
* @returns The TTL of the cached data, or `undefined` if the data does not exist.
|
|
49
|
-
*/
|
|
50
|
-
ttl(key: string): Promise<number | undefined>;
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Caches data under the given key and an optional TTL.
|
|
54
|
-
* @typeParam T The type of the data to cache.
|
|
55
|
-
*
|
|
56
|
-
* @param key The key of the cached data.
|
|
57
|
-
* @param data The data to cache.
|
|
58
|
-
* @param ttl The optional TTL of the cached data. If not defined, the data is cached forever.
|
|
59
|
-
* @returns The cached data.
|
|
60
|
-
*/
|
|
61
|
-
set<T>(key: string, data: T, ttl?: number): Promise<T>;
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Caches data under the given list of keys and an optional TTL.
|
|
65
|
-
* @typeParam T The type of the data to cache.
|
|
66
|
-
*
|
|
67
|
-
* @param data The list of data to cache.
|
|
68
|
-
* @returns The list of cached data.
|
|
69
|
-
*/
|
|
70
|
-
mset<T>(
|
|
71
|
-
data: Array<{
|
|
72
|
-
key: string;
|
|
73
|
-
value: T;
|
|
74
|
-
ttl?: number;
|
|
75
|
-
}>
|
|
76
|
-
): Promise<
|
|
77
|
-
Array<{
|
|
78
|
-
key: string;
|
|
79
|
-
value: T;
|
|
80
|
-
ttl?: number;
|
|
81
|
-
}>
|
|
82
|
-
>;
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Deletes the cached data with the given key.
|
|
86
|
-
* @typeParam T The type of the data to cache.
|
|
87
|
-
*
|
|
88
|
-
* @param key The key of the cached data.
|
|
89
|
-
* @returns `true` if the data was deleted, `false` otherwise.
|
|
90
|
-
*/
|
|
91
|
-
del(key: string): Promise<boolean>;
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Deletes the cached data with the given list of keys.
|
|
95
|
-
* @typeParam T The type of the data to cache.
|
|
96
|
-
*
|
|
97
|
-
* @param keys The list of cached data keys.
|
|
98
|
-
* @returns `true` if the data was deleted, `false` otherwise.
|
|
99
|
-
*/
|
|
100
|
-
mdel(keys: string[]): Promise<boolean>;
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Deletes all cached data.
|
|
104
|
-
* @returns `true` if the data was deleted, `false` otherwise.
|
|
105
|
-
*/
|
|
106
|
-
clear(): Promise<boolean>;
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Creates a sub-cache with the given tags.
|
|
110
|
-
* @param tags The list of tags for the sub-cache.
|
|
111
|
-
*/
|
|
112
|
-
tags(...tags: string[]): Omit<CacheInterface, 'tags'>;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Makes a cache interface wrapping the given store.
|
|
117
|
-
* @author Axel Nana <axel.nana@workburd.com>
|
|
118
|
-
* @param store The cache storage to use in the interface.
|
|
119
|
-
* @returns A new `CacheInterface`.
|
|
120
|
-
*/
|
|
121
|
-
const makeCacheInterface = (store: KeyvStoreAdapter): CacheInterface => {
|
|
122
|
-
const base = createCache({
|
|
123
|
-
stores: [
|
|
124
|
-
new Keyv({
|
|
125
|
-
store,
|
|
126
|
-
namespace: 'cache'
|
|
127
|
-
})
|
|
128
|
-
]
|
|
129
|
-
}) as Omit<CacheInterface, 'tags'>;
|
|
130
|
-
|
|
131
|
-
const tagsCache = new Map<string, Omit<CacheInterface, 'tags'>>();
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
...base,
|
|
135
|
-
tags(...tags) {
|
|
136
|
-
const namespace = `cache__${alphabetical(tags, (item) => item).join('__')}`;
|
|
137
|
-
let cache = tagsCache.get(namespace);
|
|
138
|
-
|
|
139
|
-
if (!cache) {
|
|
140
|
-
cache = createCache({
|
|
141
|
-
stores: [
|
|
142
|
-
new Keyv({
|
|
143
|
-
store,
|
|
144
|
-
namespace
|
|
145
|
-
})
|
|
146
|
-
]
|
|
147
|
-
}) as Omit<CacheInterface, 'tags'>;
|
|
148
|
-
|
|
149
|
-
tagsCache.set(namespace, cache);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return cache;
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
export abstract class Cache {
|
|
158
|
-
static #redis: CacheInterface | undefined;
|
|
159
|
-
static #memory: CacheInterface | undefined;
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* A weak cache storage for simple non-intrusive caching of data.
|
|
163
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
164
|
-
*/
|
|
165
|
-
public static readonly weak = Object.seal({
|
|
166
|
-
/**
|
|
167
|
-
* INTERNAL USAGE ONLY.
|
|
168
|
-
* @private
|
|
169
|
-
*/
|
|
170
|
-
map: new WeakMap<object, unknown>(),
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Gets the cached data for a given context.
|
|
174
|
-
* @param ctx The cache context.
|
|
175
|
-
* @returns The cached data.
|
|
176
|
-
*/
|
|
177
|
-
get<T extends object>(ctx: object): T {
|
|
178
|
-
if (!this.map.has(ctx)) this.map.set(ctx, {});
|
|
179
|
-
return this.map.get(ctx) as T;
|
|
180
|
-
},
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Deletes the cached data for a given context.
|
|
184
|
-
* @param ctx The cache context.
|
|
185
|
-
*/
|
|
186
|
-
del(ctx: object): void {
|
|
187
|
-
this.map.delete(ctx);
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Redis-based cache storage.
|
|
193
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
194
|
-
*/
|
|
195
|
-
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;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Memory-based cache storage.
|
|
209
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
210
|
-
*/
|
|
211
|
-
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
|
-
}
|
|
218
|
-
}
|
package/src/database.ts
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025-present Workbud Technologies Inc. All rights reserved.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
import type { CacheInterface } from './cache';
|
|
16
|
-
import type { DatabaseCacheConfig, DatabaseDriver } from './interfaces';
|
|
17
|
-
|
|
18
|
-
import { Application, Service } from '@elysiumjs/core';
|
|
19
|
-
|
|
20
|
-
import { Cache } from './cache';
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Creates a cache storage instance tagged for database use.
|
|
24
|
-
* Drivers can use this to build their cache backends.
|
|
25
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
26
|
-
* @param config The database cache configuration.
|
|
27
|
-
* @returns A cache interface tagged with 'database'.
|
|
28
|
-
*/
|
|
29
|
-
export const createDatabaseCacheStorage = (
|
|
30
|
-
config: DatabaseCacheConfig
|
|
31
|
-
): Omit<CacheInterface, 'tags'> => {
|
|
32
|
-
return Cache[config.storage ?? 'redis'].tags('database');
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Abstract database manager.
|
|
37
|
-
*
|
|
38
|
-
* Manages named database connections using a driver. Connections are
|
|
39
|
-
* registered in the service container and can be retrieved by name.
|
|
40
|
-
*
|
|
41
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
42
|
-
* @template TConnection The connection type managed by the driver.
|
|
43
|
-
* @template TConnectionConfig The configuration used to create a connection.
|
|
44
|
-
*/
|
|
45
|
-
export abstract class AbstractDatabase<TConnection, TConnectionConfig> {
|
|
46
|
-
/**
|
|
47
|
-
* The database driver used to create and manage connections.
|
|
48
|
-
*/
|
|
49
|
-
protected abstract driver: DatabaseDriver<TConnection, TConnectionConfig>;
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Creates a service name for a database connection.
|
|
53
|
-
* @param name The name of the connection.
|
|
54
|
-
* @returns A service name for the connection.
|
|
55
|
-
*/
|
|
56
|
-
private getConnectionName(name: string): string {
|
|
57
|
-
return `db.connection.${name}`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Retrieves the connection with the given name.
|
|
62
|
-
*
|
|
63
|
-
* For the default connection, checks the async context for a
|
|
64
|
-
* transaction handle (`db:tx`) before falling back to the service container.
|
|
65
|
-
*
|
|
66
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
67
|
-
* @param name The connection name.
|
|
68
|
-
* @returns The connection with the given name.
|
|
69
|
-
*/
|
|
70
|
-
public getConnection(name: string = 'default'): TConnection {
|
|
71
|
-
if (name === 'default') {
|
|
72
|
-
const tx = Application.context.getStore()?.get('db:tx') as TConnection | undefined;
|
|
73
|
-
if (tx) return tx;
|
|
74
|
-
}
|
|
75
|
-
|
|
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
|
-
}
|
|
106
|
-
|
|
107
|
-
const connection = this.driver.createConnection(config);
|
|
108
|
-
return Service.instance(serviceName, connection);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Checks if a connection with the given name exists.
|
|
113
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
114
|
-
* @param name The name of the connection to check.
|
|
115
|
-
* @returns `true` if the connection exists, `false` otherwise.
|
|
116
|
-
*/
|
|
117
|
-
public connectionExists(name: string): boolean {
|
|
118
|
-
return Service.exists(this.getConnectionName(name));
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Retrieves the default connection.
|
|
123
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
124
|
-
* @returns The default connection.
|
|
125
|
-
*/
|
|
126
|
-
public getDefaultConnection(): TConnection {
|
|
127
|
-
return this.getConnection('default');
|
|
128
|
-
}
|
|
129
|
-
|
|
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
|
-
/**
|
|
143
|
-
* Returns the raw underlying client for the given connection.
|
|
144
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
145
|
-
* @param name The connection name.
|
|
146
|
-
* @returns The raw client instance.
|
|
147
|
-
*/
|
|
148
|
-
public getRawClient(name: string = 'default'): unknown {
|
|
149
|
-
return this.driver.getRawClient(this.getConnection(name));
|
|
150
|
-
}
|
|
151
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026-present Workbud Technologies Inc. All rights reserved.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
import type { TenancyConfig } from './tenancy';
|
|
16
|
-
|
|
17
|
-
export type {
|
|
18
|
-
ColumnMetadata,
|
|
19
|
-
DatabaseCacheConfig,
|
|
20
|
-
DatabaseCacheStrategy,
|
|
21
|
-
DatabaseDriver,
|
|
22
|
-
IdType,
|
|
23
|
-
ModelAdapter,
|
|
24
|
-
ModelClass,
|
|
25
|
-
RepositoryClass,
|
|
26
|
-
RepositoryInterface,
|
|
27
|
-
TenancyStrategy
|
|
28
|
-
} from './interfaces';
|
|
29
|
-
|
|
30
|
-
export * from './database';
|
|
31
|
-
export * from './model';
|
|
32
|
-
export * from './repository';
|
|
33
|
-
|
|
34
|
-
export * from './tenancy';
|
|
35
|
-
export type { TenancyConfig, TenancyMode, ModelTenancyConfig } from './tenancy';
|
|
36
|
-
|
|
37
|
-
export * from './cache';
|
|
38
|
-
export * from './keyv-redis';
|
|
39
|
-
|
|
40
|
-
// Configuration types
|
|
41
|
-
export interface MnemosyneConfig<TConnectionConfig = unknown> {
|
|
42
|
-
database?: {
|
|
43
|
-
default: string;
|
|
44
|
-
connections: Record<string, TConnectionConfig>;
|
|
45
|
-
};
|
|
46
|
-
tenancy?: TenancyConfig;
|
|
47
|
-
}
|